public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH] Fast COPY FROM into the foreign (or sharded) table.
62+ messages / 6 participants
[nested] [flat]

* [PATCH] Fast COPY FROM into the foreign (or sharded) table.
@ 2020-05-29 05:39  Andrey V. Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey V. Lepikhov @ 2020-05-29 05:39 UTC (permalink / raw)

---
 contrib/postgres_fdw/deparse.c                |  25 ++
 .../postgres_fdw/expected/postgres_fdw.out    |   5 +-
 contrib/postgres_fdw/postgres_fdw.c           |  95 ++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 src/backend/commands/copy.c                   | 213 ++++++++++++------
 src/include/commands/copy.h                   |   5 +
 src/include/foreign/fdwapi.h                  |   9 +
 7 files changed, 286 insertions(+), 67 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..427402c8eb 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -1758,6 +1758,31 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	int attnum;
+
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	appendStringInfoString(buf, " ( ");
+
+	for(attnum = 0; attnum < rel->rd_att->natts; attnum++)
+	{
+		appendStringInfoString(buf, NameStr(rel->rd_att->attrs[attnum].attname));
+
+		if (attnum != rel->rd_att->natts-1)
+			appendStringInfoString(buf, ", ");
+	}
+
+	appendStringInfoString(buf, " ) ");
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..5ae24fef7c 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2 ( f1, f2 )  FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..bd2a8f596f 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState fdwcstate;
 } PgFdwModifyState;
 
 /*
@@ -350,12 +352,16 @@ static TupleTableSlot *postgresExecForeignDelete(EState *estate,
 												 ResultRelInfo *resultRelInfo,
 												 TupleTableSlot *slot,
 												 TupleTableSlot *planSlot);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+												 TupleTableSlot *slot);
 static void postgresEndForeignModify(EState *estate,
 									 ResultRelInfo *resultRelInfo);
 static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(ResultRelInfo *resultRelInfo, bool status);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -530,9 +536,12 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->ExecForeignInsert = postgresExecForeignInsert;
 	routine->ExecForeignUpdate = postgresExecForeignUpdate;
 	routine->ExecForeignDelete = postgresExecForeignDelete;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -1890,6 +1899,27 @@ postgresExecForeignDelete(EState *estate,
 								  slot, planSlot);
 }
 
+/*
+ * postgresExecForeignCopy
+ *		Copy one row into a foreign table
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo, TupleTableSlot *slot)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	char *buf;
+
+	buf = NextForeignCopyRow(fmstate->fdwcstate, slot);
+
+	if (PQputCopyData(fmstate->conn, buf, strlen(buf)) <= 0)
+	{
+		PGresult *res;
+
+		res = PQgetResult(fmstate->conn);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pgfdw_report_error(ERROR, res, fmstate->conn, false, fmstate->query);
+	}
+}
 /*
  * postgresEndForeignModify
  *		Finish an insert/update/delete operation on a foreign table
@@ -2051,6 +2081,71 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ResultRelInfo *resultRelInfo)
+{
+	Relation rel = resultRelInfo->ri_RelationDesc;
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) (resultRelInfo->ri_FdwState);
+	StringInfoData sql;
+	PGresult *res;
+
+	Assert(resultRelInfo->ri_FdwRoutine != NULL);
+
+	fmstate->target_attrs = NULL;
+	fmstate->has_returning = false;
+	fmstate->retrieved_attrs = NULL;
+
+	if (fmstate->fdwcstate == NULL)
+		fmstate->fdwcstate = BeginForeignCopyTo(rel);
+
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+	fmstate->query = sql.data;
+
+	res = PQexec(fmstate->conn, fmstate->query);
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(ResultRelInfo *resultRelInfo, bool status)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+	PGresult *res;
+
+	Assert(fmstate != NULL);
+
+	if (!status)
+	{
+		PQputCopyEnd(fmstate->conn, (PQprotocolVersion(fmstate->conn) < 3) ?
+					NULL :
+					_("aborted foreign copy"));
+		pfree(fmstate->fdwcstate);
+		fmstate->fdwcstate = NULL;
+		EndForeignCopyTo(fmstate->fdwcstate);
+		return;
+	}
+
+	while (res = PQgetResult(fmstate->conn), PQresultStatus(res) == PGRES_COPY_IN)
+	{
+		/* We can't send an error message if we're using protocol version 2 */
+		PQputCopyEnd(fmstate->conn, (status || PQprotocolVersion(fmstate->conn) < 3) ? NULL :
+					 _("aborted foreign copy"));
+		PQclear(res);
+	}
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pgfdw_report_error(ERROR, res, fmstate->conn, false, fmstate->query);
+
+	while (PQgetResult(fmstate->conn) != NULL);
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 6d53dc463c..87e0f46846 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -133,6 +133,7 @@ typedef struct CopyStateData
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -358,8 +359,11 @@ static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
 static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
 							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
 static void EndCopyTo(CopyState cstate);
+static void CopyToStart(CopyState cstate);
+static void CopyToFinish(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
 static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
@@ -587,7 +591,9 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			CopySendChar(cstate, '\0');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1075,7 +1081,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	else
 	{
 		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1815,6 +1821,32 @@ EndCopy(CopyState cstate)
 	pfree(cstate);
 }
 
+static char *buf = NULL;
+static void
+data_dest_cb(void *outbuf, int len)
+{
+	buf = (char *) palloc(len);
+	memcpy(buf, (char *) outbuf, len);
+}
+
+CopyState
+BeginForeignCopyTo(Relation rel)
+{
+	CopyState cstate;
+
+	cstate = BeginCopy(NULL, false, rel, NULL, InvalidOid, NIL, NIL);
+	cstate->copy_dest = COPY_CALLBACK;
+	cstate->data_dest_cb = data_dest_cb;
+	CopyToStart(cstate);
+	return cstate;
+}
+
+void
+EndForeignCopyTo(CopyState cstate)
+{
+	CopyToFinish(cstate);
+}
+
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
@@ -1825,6 +1857,7 @@ BeginCopyTo(ParseState *pstate,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
@@ -1880,6 +1913,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -1950,6 +1988,13 @@ BeginCopyTo(ParseState *pstate,
 	return cstate;
 }
 
+char *
+NextForeignCopyRow(CopyState cstate, TupleTableSlot *slot)
+{
+	CopyOneRowTo(cstate, slot);
+	return buf;
+}
+
 /*
  * This intermediate routine exists mainly to localize the effects of setjmp
  * so we don't need to plaster a lot of variables with "volatile".
@@ -1966,7 +2011,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2005,16 +2052,12 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
- */
-static uint64
-CopyTo(CopyState cstate)
+static void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
@@ -2104,6 +2147,29 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+static void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2135,17 +2201,6 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
@@ -2449,53 +2504,82 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
 	{
-		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
-		 */
-		if (resultRelInfo->ri_NumIndices > 0)
+		/* Flush into foreign table or partition */
+		int i;
+		bool status = false;
+
+		Assert(resultRelInfo->ri_FdwRoutine != NULL &&
+			   resultRelInfo->ri_FdwState != NULL);
+
+		PG_TRY();
 		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
+			resultRelInfo->ri_FdwRoutine->BeginForeignCopy(resultRelInfo);
+			for (i = 0; i < nused; i++)
+				resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+															  slots[i]);
+			status = true;
 		}
-
+		PG_FINALLY();
+		{
+			resultRelInfo->ri_FdwRoutine->EndForeignCopy(
+														buffer->resultRelInfo,
+														status);
+		}
+		PG_END_TRY();
+	}
+	else
+	{
 		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
+
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2868,8 +2952,7 @@ CopyFrom(CopyState cstate)
 		 */
 		insertMethod = CIM_SINGLE;
 	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
+	else if (cstate->volatile_defexprs)
 	{
 		/*
 		 * Can't support multi-inserts to foreign tables or if there are any
@@ -3037,8 +3120,7 @@ CopyFrom(CopyState cstate)
 				 */
 				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
 					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
+					!has_instead_insert_row_trig;
 
 				/* Set the multi-insert buffer to use for this partition. */
 				if (leafpart_use_multi_insert)
@@ -3048,7 +3130,8 @@ CopyFrom(CopyState cstate)
 													   resultRelInfo);
 				}
 				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo) &&
+						 resultRelInfo->ri_FdwRoutine == NULL)
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..ef119a761a 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -41,4 +42,8 @@ extern uint64 CopyFrom(CopyState cstate);
 
 extern DestReceiver *CreateCopyDestReceiver(void);
 
+extern CopyState BeginForeignCopyTo(Relation rel);
+extern char *NextForeignCopyRow(CopyState cstate, TupleTableSlot *slot);
+extern void EndForeignCopyTo(CopyState cstate);
+
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..197301c5a5 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -94,6 +94,8 @@ typedef TupleTableSlot *(*ExecForeignDelete_function) (EState *estate,
 													   ResultRelInfo *rinfo,
 													   TupleTableSlot *slot,
 													   TupleTableSlot *planSlot);
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot *slot);
 
 typedef void (*EndForeignModify_function) (EState *estate,
 										   ResultRelInfo *rinfo);
@@ -104,6 +106,10 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopy_function) (ResultRelInfo *rinfo, bool status);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -211,9 +217,12 @@ typedef struct FdwRoutine
 	ExecForeignInsert_function ExecForeignInsert;
 	ExecForeignUpdate_function ExecForeignUpdate;
 	ExecForeignDelete_function ExecForeignDelete;
+	ExecForeignCopy_function ExecForeignCopy;
 	EndForeignModify_function EndForeignModify;
 	BeginForeignInsert_function BeginForeignInsert;
 	EndForeignInsert_function EndForeignInsert;
+	BeginForeignCopy_function BeginForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
 	IsForeignRelUpdatable_function IsForeignRelUpdatable;
 	PlanDirectModify_function PlanDirectModify;
 	BeginDirectModify_function BeginDirectModify;
-- 
2.17.1


--------------6CBD6387258C92F2F8396A1D--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign (or sharded) table.
@ 2020-05-29 05:39  Andrey V. Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey V. Lepikhov @ 2020-05-29 05:39 UTC (permalink / raw)

---
 contrib/postgres_fdw/deparse.c                |  25 ++
 .../postgres_fdw/expected/postgres_fdw.out    |   5 +-
 contrib/postgres_fdw/postgres_fdw.c           |  95 ++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 src/backend/commands/copy.c                   | 216 ++++++++++++------
 src/include/commands/copy.h                   |   5 +
 src/include/foreign/fdwapi.h                  |   9 +
 7 files changed, 289 insertions(+), 67 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..427402c8eb 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -1758,6 +1758,31 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	int attnum;
+
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	appendStringInfoString(buf, " ( ");
+
+	for(attnum = 0; attnum < rel->rd_att->natts; attnum++)
+	{
+		appendStringInfoString(buf, NameStr(rel->rd_att->attrs[attnum].attname));
+
+		if (attnum != rel->rd_att->natts-1)
+			appendStringInfoString(buf, ", ");
+	}
+
+	appendStringInfoString(buf, " ) ");
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..5ae24fef7c 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2 ( f1, f2 )  FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..bd2a8f596f 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState fdwcstate;
 } PgFdwModifyState;
 
 /*
@@ -350,12 +352,16 @@ static TupleTableSlot *postgresExecForeignDelete(EState *estate,
 												 ResultRelInfo *resultRelInfo,
 												 TupleTableSlot *slot,
 												 TupleTableSlot *planSlot);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+												 TupleTableSlot *slot);
 static void postgresEndForeignModify(EState *estate,
 									 ResultRelInfo *resultRelInfo);
 static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(ResultRelInfo *resultRelInfo, bool status);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -530,9 +536,12 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->ExecForeignInsert = postgresExecForeignInsert;
 	routine->ExecForeignUpdate = postgresExecForeignUpdate;
 	routine->ExecForeignDelete = postgresExecForeignDelete;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -1890,6 +1899,27 @@ postgresExecForeignDelete(EState *estate,
 								  slot, planSlot);
 }
 
+/*
+ * postgresExecForeignCopy
+ *		Copy one row into a foreign table
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo, TupleTableSlot *slot)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	char *buf;
+
+	buf = NextForeignCopyRow(fmstate->fdwcstate, slot);
+
+	if (PQputCopyData(fmstate->conn, buf, strlen(buf)) <= 0)
+	{
+		PGresult *res;
+
+		res = PQgetResult(fmstate->conn);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pgfdw_report_error(ERROR, res, fmstate->conn, false, fmstate->query);
+	}
+}
 /*
  * postgresEndForeignModify
  *		Finish an insert/update/delete operation on a foreign table
@@ -2051,6 +2081,71 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ResultRelInfo *resultRelInfo)
+{
+	Relation rel = resultRelInfo->ri_RelationDesc;
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) (resultRelInfo->ri_FdwState);
+	StringInfoData sql;
+	PGresult *res;
+
+	Assert(resultRelInfo->ri_FdwRoutine != NULL);
+
+	fmstate->target_attrs = NULL;
+	fmstate->has_returning = false;
+	fmstate->retrieved_attrs = NULL;
+
+	if (fmstate->fdwcstate == NULL)
+		fmstate->fdwcstate = BeginForeignCopyTo(rel);
+
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+	fmstate->query = sql.data;
+
+	res = PQexec(fmstate->conn, fmstate->query);
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(ResultRelInfo *resultRelInfo, bool status)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+	PGresult *res;
+
+	Assert(fmstate != NULL);
+
+	if (!status)
+	{
+		PQputCopyEnd(fmstate->conn, (PQprotocolVersion(fmstate->conn) < 3) ?
+					NULL :
+					_("aborted foreign copy"));
+		pfree(fmstate->fdwcstate);
+		fmstate->fdwcstate = NULL;
+		EndForeignCopyTo(fmstate->fdwcstate);
+		return;
+	}
+
+	while (res = PQgetResult(fmstate->conn), PQresultStatus(res) == PGRES_COPY_IN)
+	{
+		/* We can't send an error message if we're using protocol version 2 */
+		PQputCopyEnd(fmstate->conn, (status || PQprotocolVersion(fmstate->conn) < 3) ? NULL :
+					 _("aborted foreign copy"));
+		PQclear(res);
+	}
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pgfdw_report_error(ERROR, res, fmstate->conn, false, fmstate->query);
+
+	while (PQgetResult(fmstate->conn) != NULL);
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 6d53dc463c..9459e031c7 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -133,6 +133,7 @@ typedef struct CopyStateData
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -358,8 +359,11 @@ static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
 static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
 							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
 static void EndCopyTo(CopyState cstate);
+static void CopyToStart(CopyState cstate);
+static void CopyToFinish(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
 static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
@@ -587,7 +591,9 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			CopySendChar(cstate, '\0');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1075,7 +1081,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	else
 	{
 		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1815,6 +1821,32 @@ EndCopy(CopyState cstate)
 	pfree(cstate);
 }
 
+static char *buf = NULL;
+static void
+data_dest_cb(void *outbuf, int len)
+{
+	buf = (char *) palloc(len);
+	memcpy(buf, (char *) outbuf, len);
+}
+
+CopyState
+BeginForeignCopyTo(Relation rel)
+{
+	CopyState cstate;
+
+	cstate = BeginCopy(NULL, false, rel, NULL, InvalidOid, NIL, NIL);
+	cstate->copy_dest = COPY_CALLBACK;
+	cstate->data_dest_cb = data_dest_cb;
+	CopyToStart(cstate);
+	return cstate;
+}
+
+void
+EndForeignCopyTo(CopyState cstate)
+{
+	CopyToFinish(cstate);
+}
+
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
@@ -1825,6 +1857,7 @@ BeginCopyTo(ParseState *pstate,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
@@ -1880,6 +1913,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -1950,6 +1988,13 @@ BeginCopyTo(ParseState *pstate,
 	return cstate;
 }
 
+char *
+NextForeignCopyRow(CopyState cstate, TupleTableSlot *slot)
+{
+	CopyOneRowTo(cstate, slot);
+	return buf;
+}
+
 /*
  * This intermediate routine exists mainly to localize the effects of setjmp
  * so we don't need to plaster a lot of variables with "volatile".
@@ -1966,7 +2011,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2005,16 +2052,12 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
- */
-static uint64
-CopyTo(CopyState cstate)
+static void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
@@ -2104,6 +2147,29 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+static void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2135,17 +2201,6 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
@@ -2449,53 +2504,85 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
 	{
-		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
-		 */
-		if (resultRelInfo->ri_NumIndices > 0)
+		/* Flush into foreign table or partition */
+		int i;
+		bool status = false;
+		CopyState fcstate = NULL;
+
+		Assert(resultRelInfo->ri_FdwRoutine != NULL &&
+			   resultRelInfo->ri_FdwState != NULL);
+
+		PG_TRY();
 		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
+			fcstate = resultRelInfo->ri_FdwRoutine->BeginForeignCopy(resultRelInfo);
+			for (i = 0; i < nused; i++)
+				resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+															  fcstate,
+															  slots[i]);
+			status = true;
 		}
-
+		PG_FINALLY();
+		{
+			Assert(fcstate != NULL);
+			resultRelInfo->ri_FdwRoutine->EndForeignCopy(
+														buffer->resultRelInfo,
+														status);
+		}
+		PG_END_TRY();
+	}
+	else
+	{
 		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
+
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2868,8 +2955,7 @@ CopyFrom(CopyState cstate)
 		 */
 		insertMethod = CIM_SINGLE;
 	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
+	else if (cstate->volatile_defexprs)
 	{
 		/*
 		 * Can't support multi-inserts to foreign tables or if there are any
@@ -3037,8 +3123,7 @@ CopyFrom(CopyState cstate)
 				 */
 				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
 					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
+					!has_instead_insert_row_trig;
 
 				/* Set the multi-insert buffer to use for this partition. */
 				if (leafpart_use_multi_insert)
@@ -3048,7 +3133,8 @@ CopyFrom(CopyState cstate)
 													   resultRelInfo);
 				}
 				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo) &&
+						 resultRelInfo->ri_FdwRoutine == NULL)
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..ef119a761a 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -41,4 +42,8 @@ extern uint64 CopyFrom(CopyState cstate);
 
 extern DestReceiver *CreateCopyDestReceiver(void);
 
+extern CopyState BeginForeignCopyTo(Relation rel);
+extern char *NextForeignCopyRow(CopyState cstate, TupleTableSlot *slot);
+extern void EndForeignCopyTo(CopyState cstate);
+
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..197301c5a5 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -94,6 +94,8 @@ typedef TupleTableSlot *(*ExecForeignDelete_function) (EState *estate,
 													   ResultRelInfo *rinfo,
 													   TupleTableSlot *slot,
 													   TupleTableSlot *planSlot);
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot *slot);
 
 typedef void (*EndForeignModify_function) (EState *estate,
 										   ResultRelInfo *rinfo);
@@ -104,6 +106,10 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopy_function) (ResultRelInfo *rinfo, bool status);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -211,9 +217,12 @@ typedef struct FdwRoutine
 	ExecForeignInsert_function ExecForeignInsert;
 	ExecForeignUpdate_function ExecForeignUpdate;
 	ExecForeignDelete_function ExecForeignDelete;
+	ExecForeignCopy_function ExecForeignCopy;
 	EndForeignModify_function EndForeignModify;
 	BeginForeignInsert_function BeginForeignInsert;
 	EndForeignInsert_function EndForeignInsert;
+	BeginForeignCopy_function BeginForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
 	IsForeignRelUpdatable_function IsForeignRelUpdatable;
 	PlanDirectModify_function PlanDirectModify;
 	BeginDirectModify_function BeginDirectModify;
-- 
2.17.1


--------------4EE45C329DD5CAFC30FE5A7A--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-06-17 06:07  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-06-17 06:07 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.
---
 contrib/postgres_fdw/deparse.c                |  60 ++++-
 .../postgres_fdw/expected/postgres_fdw.out    |  33 ++-
 contrib/postgres_fdw/postgres_fdw.c           |  98 ++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  28 +++
 src/backend/commands/copy.c                   | 223 ++++++++++++------
 src/include/commands/copy.h                   |   5 +
 src/include/foreign/fdwapi.h                  |   9 +
 8 files changed, 374 insertions(+), 83 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 82fc1290ef..3a3cca5047 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8183,6 +8184,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..45441f3441 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState fdwcstate;
 } PgFdwModifyState;
 
 /*
@@ -350,12 +352,16 @@ static TupleTableSlot *postgresExecForeignDelete(EState *estate,
 												 ResultRelInfo *resultRelInfo,
 												 TupleTableSlot *slot,
 												 TupleTableSlot *planSlot);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+												 TupleTableSlot *slot);
 static void postgresEndForeignModify(EState *estate,
 									 ResultRelInfo *resultRelInfo);
 static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(ResultRelInfo *resultRelInfo, bool status);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -530,9 +536,12 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->ExecForeignInsert = postgresExecForeignInsert;
 	routine->ExecForeignUpdate = postgresExecForeignUpdate;
 	routine->ExecForeignDelete = postgresExecForeignDelete;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -1890,6 +1899,27 @@ postgresExecForeignDelete(EState *estate,
 								  slot, planSlot);
 }
 
+/*
+ * postgresExecForeignCopy
+ *		Copy one row into a foreign table
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo, TupleTableSlot *slot)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	char *buf;
+
+	buf = NextForeignCopyRow(fmstate->fdwcstate, slot);
+
+	if (PQputCopyData(fmstate->conn, buf, strlen(buf)) <= 0)
+	{
+		PGresult *res;
+
+		res = PQgetResult(fmstate->conn);
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+			pgfdw_report_error(ERROR, res, fmstate->conn, false, fmstate->query);
+	}
+}
 /*
  * postgresEndForeignModify
  *		Finish an insert/update/delete operation on a foreign table
@@ -2051,6 +2081,74 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ResultRelInfo *resultRelInfo)
+{
+	Relation rel = resultRelInfo->ri_RelationDesc;
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) (resultRelInfo->ri_FdwState);
+	StringInfoData sql;
+	PGresult *res;
+
+	Assert(resultRelInfo->ri_FdwRoutine != NULL);
+
+	fmstate->target_attrs = NULL;
+	fmstate->has_returning = false;
+	fmstate->retrieved_attrs = NULL;
+
+	if (fmstate->fdwcstate == NULL)
+		fmstate->fdwcstate = BeginForeignCopyTo(rel);
+
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+	fmstate->query = sql.data;
+
+	res = PQexec(fmstate->conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, fmstate->conn, false, fmstate->query);
+	PQclear(res);
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(ResultRelInfo *resultRelInfo, bool status)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+	PGresult *res;
+
+	Assert(fmstate != NULL);
+
+	if (!status)
+	{
+		PQputCopyEnd(fmstate->conn, (PQprotocolVersion(fmstate->conn) < 3) ?
+					NULL :
+					_("aborted foreign copy"));
+		pfree(fmstate->fdwcstate);
+		fmstate->fdwcstate = NULL;
+		EndForeignCopyTo(fmstate->fdwcstate);
+		return;
+	}
+
+	while (res = PQgetResult(fmstate->conn), PQresultStatus(res) == PGRES_COPY_IN)
+	{
+		/* We can't send an error message if we're using protocol version 2 */
+		PQputCopyEnd(fmstate->conn, (status || PQprotocolVersion(fmstate->conn) < 3) ? NULL :
+					 _("aborted foreign copy"));
+		PQclear(res);
+	}
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+		pgfdw_report_error(ERROR, res, fmstate->conn, false, fmstate->query);
+
+	while (PQgetResult(fmstate->conn) != NULL);
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..73f98a3152 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2293,6 +2293,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 6d53dc463c..1be164b3ca 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -133,6 +133,7 @@ typedef struct CopyStateData
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -358,8 +359,11 @@ static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
 static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
 							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
 static void EndCopyTo(CopyState cstate);
+static void CopyToStart(CopyState cstate);
+static void CopyToFinish(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
 static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
@@ -587,7 +591,9 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			CopySendChar(cstate, '\0');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1075,7 +1081,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	else
 	{
 		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1815,6 +1821,32 @@ EndCopy(CopyState cstate)
 	pfree(cstate);
 }
 
+static char *buf = NULL;
+static void
+data_dest_cb(void *outbuf, int len)
+{
+	buf = (char *) palloc(len);
+	memcpy(buf, (char *) outbuf, len);
+}
+
+CopyState
+BeginForeignCopyTo(Relation rel)
+{
+	CopyState cstate;
+
+	cstate = BeginCopy(NULL, false, rel, NULL, InvalidOid, NIL, NIL);
+	cstate->copy_dest = COPY_CALLBACK;
+	cstate->data_dest_cb = data_dest_cb;
+	CopyToStart(cstate);
+	return cstate;
+}
+
+void
+EndForeignCopyTo(CopyState cstate)
+{
+	CopyToFinish(cstate);
+}
+
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
@@ -1825,6 +1857,7 @@ BeginCopyTo(ParseState *pstate,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
@@ -1880,6 +1913,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -1950,6 +1988,13 @@ BeginCopyTo(ParseState *pstate,
 	return cstate;
 }
 
+char *
+NextForeignCopyRow(CopyState cstate, TupleTableSlot *slot)
+{
+	CopyOneRowTo(cstate, slot);
+	return buf;
+}
+
 /*
  * This intermediate routine exists mainly to localize the effects of setjmp
  * so we don't need to plaster a lot of variables with "volatile".
@@ -1966,7 +2011,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2005,16 +2052,12 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
- */
-static uint64
-CopyTo(CopyState cstate)
+static void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
@@ -2104,6 +2147,29 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+static void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2135,17 +2201,6 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
@@ -2449,53 +2504,83 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
 	{
-		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
-		 */
-		if (resultRelInfo->ri_NumIndices > 0)
+		/* Flush into foreign table or partition */
+		int i;
+		bool status = false;
+
+		Assert(resultRelInfo->ri_FdwRoutine != NULL &&
+			   resultRelInfo->ri_FdwState != NULL);
+
+		resultRelInfo->ri_FdwRoutine->BeginForeignCopy(resultRelInfo);
+
+		PG_TRY();
 		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
+			for (i = 0; i < nused; i++)
+				resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+															  slots[i]);
+			status = true;
 		}
-
+		PG_FINALLY();
+		{
+			resultRelInfo->ri_FdwRoutine->EndForeignCopy(
+														buffer->resultRelInfo,
+														status);
+		}
+		PG_END_TRY();
+	}
+	else
+	{
 		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
+
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2868,14 +2953,14 @@ CopyFrom(CopyState cstate)
 		 */
 		insertMethod = CIM_SINGLE;
 	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
+	else if (cstate->volatile_defexprs || (resultRelInfo->ri_FdwRoutine != NULL &&
+			list_length(cstate->attnumlist) == 0))
 	{
 		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
 		 *
 		 * Note: It does not matter if any partitions have any volatile
 		 * default expressions as we use the defaults from the target of the
@@ -3037,8 +3122,7 @@ CopyFrom(CopyState cstate)
 				 */
 				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
 					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
+					!has_instead_insert_row_trig;
 
 				/* Set the multi-insert buffer to use for this partition. */
 				if (leafpart_use_multi_insert)
@@ -3048,7 +3132,8 @@ CopyFrom(CopyState cstate)
 													   resultRelInfo);
 				}
 				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo) &&
+						 resultRelInfo->ri_FdwRoutine == NULL)
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..ef119a761a 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -41,4 +42,8 @@ extern uint64 CopyFrom(CopyState cstate);
 
 extern DestReceiver *CreateCopyDestReceiver(void);
 
+extern CopyState BeginForeignCopyTo(Relation rel);
+extern char *NextForeignCopyRow(CopyState cstate, TupleTableSlot *slot);
+extern void EndForeignCopyTo(CopyState cstate);
+
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..197301c5a5 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -94,6 +94,8 @@ typedef TupleTableSlot *(*ExecForeignDelete_function) (EState *estate,
 													   ResultRelInfo *rinfo,
 													   TupleTableSlot *slot,
 													   TupleTableSlot *planSlot);
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot *slot);
 
 typedef void (*EndForeignModify_function) (EState *estate,
 										   ResultRelInfo *rinfo);
@@ -104,6 +106,10 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopy_function) (ResultRelInfo *rinfo, bool status);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -211,9 +217,12 @@ typedef struct FdwRoutine
 	ExecForeignInsert_function ExecForeignInsert;
 	ExecForeignUpdate_function ExecForeignUpdate;
 	ExecForeignDelete_function ExecForeignDelete;
+	ExecForeignCopy_function ExecForeignCopy;
 	EndForeignModify_function EndForeignModify;
 	BeginForeignInsert_function BeginForeignInsert;
 	EndForeignInsert_function EndForeignInsert;
+	BeginForeignCopy_function BeginForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
 	IsForeignRelUpdatable_function IsForeignRelUpdatable;
 	PlanDirectModify_function PlanDirectModify;
 	BeginDirectModify_function BeginDirectModify;
-- 
2.25.1


--------------C0462F28F701FD8E92E6A90B--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-06-22 05:28  Andrey V. Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey V. Lepikhov @ 2020-06-22 05:28 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.
---
 contrib/postgres_fdw/deparse.c                |  60 ++++-
 .../postgres_fdw/expected/postgres_fdw.out    |  33 ++-
 contrib/postgres_fdw/postgres_fdw.c           |  87 ++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  28 +++
 src/backend/commands/copy.c                   | 206 ++++++++++++------
 src/include/commands/copy.h                   |   5 +
 src/include/foreign/fdwapi.h                  |   8 +
 8 files changed, 344 insertions(+), 84 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 82fc1290ef..3a3cca5047 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8183,6 +8184,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..2b3d7d6dfb 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState fdwcstate;
 } PgFdwModifyState;
 
 /*
@@ -350,6 +352,9 @@ static TupleTableSlot *postgresExecForeignDelete(EState *estate,
 												 ResultRelInfo *resultRelInfo,
 												 TupleTableSlot *slot,
 												 TupleTableSlot *planSlot);
+static void postgresExecForeignBulkInsert(ResultRelInfo *resultRelInfo,
+									TupleTableSlot **slots,
+									int nslots);
 static void postgresEndForeignModify(EState *estate,
 									 ResultRelInfo *resultRelInfo);
 static void postgresBeginForeignInsert(ModifyTableState *mtstate,
@@ -530,6 +535,7 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->ExecForeignInsert = postgresExecForeignInsert;
 	routine->ExecForeignUpdate = postgresExecForeignUpdate;
 	routine->ExecForeignDelete = postgresExecForeignDelete;
+	routine->ExecForeignBulkInsert = postgresExecForeignBulkInsert;
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
@@ -1890,6 +1896,87 @@ postgresExecForeignDelete(EState *estate,
 								  slot, planSlot);
 }
 
+/*
+ * postgresExecForeignBulkInsert
+ *		Copy rows into a foreign table by COPY .. FROM STDIN machinery
+ */
+static void
+postgresExecForeignBulkInsert(ResultRelInfo *resultRelInfo,
+							  TupleTableSlot **slots,
+							  int nslots)
+{
+	Relation rel = resultRelInfo->ri_RelationDesc;
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	StringInfoData sql;
+	PGresult *res;
+	bool status = false;
+	PGconn *conn = fmstate->conn;
+	int i;
+
+	Assert(resultRelInfo->ri_FdwRoutine != NULL &&
+		   resultRelInfo->ri_FdwState != NULL);
+
+	fmstate->target_attrs = NULL;
+	fmstate->has_returning = false;
+	fmstate->retrieved_attrs = NULL;
+	fmstate->fdwcstate = BeginForeignCopyTo(rel);
+
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+	fmstate->query = sql.data;
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		for (i = 0; i < nslots; i++)
+		{
+			char *buf = NextForeignCopyRow(fmstate->fdwcstate, slots[i]);
+
+			if (PQputCopyData(conn, buf, strlen(buf)) <= 0)
+			{
+				res = PQgetResult(conn);
+				pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+			}
+		}
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		EndForeignCopyTo(fmstate->fdwcstate);
+		pfree(fmstate->fdwcstate);
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
 /*
  * postgresEndForeignModify
  *		Finish an insert/update/delete operation on a foreign table
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..73f98a3152 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2293,6 +2293,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 6d53dc463c..ddf3c10146 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -133,6 +133,7 @@ typedef struct CopyStateData
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -358,8 +359,11 @@ static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
 static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
 							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
 static void EndCopyTo(CopyState cstate);
+static void CopyToStart(CopyState cstate);
+static void CopyToFinish(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
 static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
@@ -587,7 +591,9 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			CopySendChar(cstate, '\0');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1075,7 +1081,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	else
 	{
 		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1815,6 +1821,32 @@ EndCopy(CopyState cstate)
 	pfree(cstate);
 }
 
+static char *buf = NULL;
+static void
+data_dest_cb(void *outbuf, int len)
+{
+	buf = (char *) palloc(len);
+	memcpy(buf, (char *) outbuf, len);
+}
+
+CopyState
+BeginForeignCopyTo(Relation rel)
+{
+	CopyState cstate;
+
+	cstate = BeginCopy(NULL, false, rel, NULL, InvalidOid, NIL, NIL);
+	cstate->copy_dest = COPY_CALLBACK;
+	cstate->data_dest_cb = data_dest_cb;
+	CopyToStart(cstate);
+	return cstate;
+}
+
+void
+EndForeignCopyTo(CopyState cstate)
+{
+	CopyToFinish(cstate);
+}
+
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
@@ -1825,6 +1857,7 @@ BeginCopyTo(ParseState *pstate,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
@@ -1880,6 +1913,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -1950,6 +1988,13 @@ BeginCopyTo(ParseState *pstate,
 	return cstate;
 }
 
+char *
+NextForeignCopyRow(CopyState cstate, TupleTableSlot *slot)
+{
+	CopyOneRowTo(cstate, slot);
+	return buf;
+}
+
 /*
  * This intermediate routine exists mainly to localize the effects of setjmp
  * so we don't need to plaster a lot of variables with "volatile".
@@ -1966,7 +2011,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2005,16 +2052,12 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
- */
-static uint64
-CopyTo(CopyState cstate)
+static void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
@@ -2104,6 +2147,29 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+static void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2135,17 +2201,6 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
@@ -2449,53 +2504,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignBulkInsert(resultRelInfo,
+															  slots,
+															  nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2868,14 +2934,14 @@ CopyFrom(CopyState cstate)
 		 */
 		insertMethod = CIM_SINGLE;
 	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
+	else if (cstate->volatile_defexprs || (resultRelInfo->ri_FdwRoutine != NULL &&
+			list_length(cstate->attnumlist) == 0))
 	{
 		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
 		 *
 		 * Note: It does not matter if any partitions have any volatile
 		 * default expressions as we use the defaults from the target of the
@@ -3037,8 +3103,7 @@ CopyFrom(CopyState cstate)
 				 */
 				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
 					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
+					!has_instead_insert_row_trig;
 
 				/* Set the multi-insert buffer to use for this partition. */
 				if (leafpart_use_multi_insert)
@@ -3048,7 +3113,8 @@ CopyFrom(CopyState cstate)
 													   resultRelInfo);
 				}
 				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo) &&
+						 resultRelInfo->ri_FdwRoutine == NULL)
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..ef119a761a 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -41,4 +42,8 @@ extern uint64 CopyFrom(CopyState cstate);
 
 extern DestReceiver *CreateCopyDestReceiver(void);
 
+extern CopyState BeginForeignCopyTo(Relation rel);
+extern char *NextForeignCopyRow(CopyState cstate, TupleTableSlot *slot);
+extern void EndForeignCopyTo(CopyState cstate);
+
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..0507c2f96f 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -94,6 +94,9 @@ typedef TupleTableSlot *(*ExecForeignDelete_function) (EState *estate,
 													   ResultRelInfo *rinfo,
 													   TupleTableSlot *slot,
 													   TupleTableSlot *planSlot);
+typedef void (*ExecForeignBulkInsert_function) (ResultRelInfo *rinfo,
+												TupleTableSlot **slots,
+												int nslots);
 
 typedef void (*EndForeignModify_function) (EState *estate,
 										   ResultRelInfo *rinfo);
@@ -104,6 +107,10 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopy_function) (ResultRelInfo *rinfo, bool status);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -211,6 +218,7 @@ typedef struct FdwRoutine
 	ExecForeignInsert_function ExecForeignInsert;
 	ExecForeignUpdate_function ExecForeignUpdate;
 	ExecForeignDelete_function ExecForeignDelete;
+	ExecForeignBulkInsert_function ExecForeignBulkInsert;
 	EndForeignModify_function EndForeignModify;
 	BeginForeignInsert_function BeginForeignInsert;
 	EndForeignInsert_function EndForeignInsert;
-- 
2.17.1


--------------87484F297D631523E6E5072D--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++-
 .../postgres_fdw/expected/postgres_fdw.out    |  33 ++-
 contrib/postgres_fdw/postgres_fdw.c           | 146 +++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  28 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++++
 src/backend/commands/copy.c                   | 247 +++++++++++-------
 src/backend/executor/execMain.c               |   1 +
 src/backend/executor/execPartition.c          |  34 ++-
 src/include/commands/copy.h                   |  11 +
 src/include/foreign/fdwapi.h                  |  15 ++
 src/include/nodes/execnodes.h                 |   8 +
 12 files changed, 547 insertions(+), 111 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..baadb4ea80 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8183,6 +8184,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..a314821fb0 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -1847,6 +1859,9 @@ postgresExecForeignInsert(EState *estate,
 	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
 	TupleTableSlot *rslot;
 
+	Assert(!resultRelInfo->ri_usesBulkModify ||
+		   resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn == NULL);
+
 	/*
 	 * If the fmstate has aux_fmstate set, use the aux_fmstate (see
 	 * postgresBeginForeignInsert())
@@ -2051,6 +2066,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..73f98a3152 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2293,6 +2293,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 44da71c4cb..2d184b2eee 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -128,11 +128,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -355,17 +358,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -589,7 +587,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1076,8 +1075,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1459,6 +1458,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1494,6 +1494,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1820,20 +1825,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1872,8 +1882,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1882,6 +1893,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -1968,7 +1984,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -1991,7 +2009,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2007,19 +2025,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2106,6 +2127,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2137,24 +2184,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2447,53 +2483,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2806,11 +2853,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2869,14 +2911,13 @@ CopyFrom(CopyState cstate)
 		 */
 		insertMethod = CIM_SINGLE;
 	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
+	else if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
 	{
 		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
 		 *
 		 * Note: It does not matter if any partitions have any volatile
 		 * default expressions as we use the defaults from the target of the
@@ -2916,6 +2957,24 @@ CopyFrom(CopyState cstate)
 								estate, mycid, ti_options);
 	}
 
+	if (insertMethod != CIM_SINGLE)
+		resultRelInfo->ri_usesBulkModify = true;
+
+	/*
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
+	 */
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesBulkModify &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
+	}
+
 	/*
 	 * If not using batch mode (which allocates slots as needed) set up a
 	 * tuple slot too. When inserting into a partitioned table, we also need
@@ -3039,7 +3098,7 @@ CopyFrom(CopyState cstate)
 				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
 					!has_before_insert_row_trig &&
 					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
+					(resultRelInfo->ri_FdwRoutine == NULL || resultRelInfo->ri_usesBulkModify);
 
 				/* Set the multi-insert buffer to use for this partition. */
 				if (leafpart_use_multi_insert)
@@ -3298,10 +3357,17 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesBulkModify &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+		target_resultRelInfo->ri_usesBulkModify = false;
+	}
 
 	/* Tear down the multi-insert buffer data */
 	if (insertMethod != CIM_SINGLE)
@@ -3354,7 +3420,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..b8b09d528e 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1345,6 +1345,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+	resultRelInfo->ri_usesBulkModify = false;
 }
 
 /*
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index fb6ce49056..1344434cf0 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -526,6 +526,11 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  rootrel,
 					  estate->es_instrument);
 
+	if (rootResultRelInfo->ri_usesBulkModify &&
+		leaf_part_rri->ri_FdwRoutine != NULL &&
+		leaf_part_rri->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+		leaf_part_rri->ri_usesBulkModify = true;
+
 	/*
 	 * Verify result relation is a valid target for an INSERT.  An UPDATE of a
 	 * partition-key becomes a DELETE+INSERT operation, so this check is still
@@ -937,9 +942,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesBulkModify)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1133,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesBulkModify)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 6f96b31fb4..de326035da 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -491,6 +491,14 @@ typedef struct ResultRelInfo
 
 	/* For use by copy.c when performing multi-inserts */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+	/*
+	 * For use by copy.c:
+	 * for partitioned relation "true" means that child relations are allowed for
+	 * using bulk modify operations; for foreign relation (or foreign partition
+	 * of) "true" value means that modify operations must use bulk FDW API.
+	 */
+	bool ri_usesBulkModify;
 } ResultRelInfo;
 
 /* ----------------
-- 
2.25.1


--------------8276B5F3D49F8615BF2BAB4C--






^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  75 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 670 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..6f86b1fa36 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..7750cd4e05 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,81 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..d9a1644f43 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..b21bc2c4f4 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopy == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..f7f7c59fae 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..a5553f1777 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopy;
+	EndForeignCopyIn_function EndForeignCopy;
+	ExecForeignCopyIn_function ExecForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.25.1


--------------74702D9E0418BB4FB3952AF3--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +++-
 contrib/postgres_fdw/postgres_fdw.c           | 143 ++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++
 doc/src/sgml/fdwhandler.sgml                  |  75 ++++++
 src/backend/commands/copy.c                   | 220 +++++++++++-------
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 ++-
 src/include/commands/copy.h                   |  11 +
 src/include/foreign/fdwapi.h                  |  15 ++
 11 files changed, 548 insertions(+), 103 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 84bc0ee381..5206814f10 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8084,8 +8084,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8096,6 +8097,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8204,6 +8218,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index a31abce7c9..9685e731e0 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2050,6 +2062,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index d452d06343..1a56432f0f 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2213,6 +2213,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2313,6 +2330,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 72fa127212..81728945ea 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,81 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 4e63926cb7..62dca2abff 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -118,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -349,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -585,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1114,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1497,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1532,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1858,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1910,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1920,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2006,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2029,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2045,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2144,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2175,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2485,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2894,10 +2941,16 @@ CopyFrom(CopyState cstate)
 	 * Init COPY into foreign table. Initialization of copying into foreign
 	 * partitions will be done later.
 	 */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
 																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -3295,10 +3348,16 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
 														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
@@ -3350,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 73f78f287a..8d31cd0f56 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1383,9 +1383,13 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 		 */
 		resultRelInfo->ri_usesMultiInsert = false;
 	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL)
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopy == NULL)
 	{
-		/* Foreign tables don't support multi-inserts. */
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		resultRelInfo->ri_usesMultiInsert = false;
 	}
 	else
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 8d01f5098d..686d20362d 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -995,9 +995,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1199,10 +1204,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..e932bdf2f4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopy_function BeginForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------0FC1AE37569A4E35540BBCE2--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.
---
 contrib/postgres_fdw/deparse.c                |  60 ++++-
 .../postgres_fdw/expected/postgres_fdw.out    |  33 ++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  28 ++
 src/backend/commands/copy.c                   | 239 ++++++++++++------
 src/backend/executor/execMain.c               |   1 +
 src/backend/executor/execPartition.c          |  34 ++-
 src/include/commands/copy.h                   |   5 +
 src/include/foreign/fdwapi.h                  |  15 ++
 src/include/nodes/execnodes.h                 |   8 +
 11 files changed, 456 insertions(+), 98 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 82fc1290ef..3a3cca5047 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8183,6 +8184,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..0db8d74320 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,10 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo);
+static TupleTableSlot *postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo, TupleTableSlot **slots, int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +539,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -1847,6 +1856,9 @@ postgresExecForeignInsert(EState *estate,
 	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
 	TupleTableSlot *rslot;
 
+	Assert(!resultRelInfo->UseBulkModifying ||
+		   resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn == NULL);
+
 	/*
 	 * If the fmstate has aux_fmstate set, use the aux_fmstate (see
 	 * postgresBeginForeignInsert())
@@ -2051,6 +2063,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	Relation	rel = resultRelInfo->ri_RelationDesc;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+	fmstate->cstate = BeginForeignCopyTo(resultRelInfo->ri_RelationDesc);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	EndForeignCopyTo(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static TupleTableSlot *
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		for (i = 0; i < nslots; i++)
+		{
+			char *buf = NextForeignCopyRow(fmstate->cstate, slots[i]);
+
+			if (PQputCopyData(conn, buf, strlen(buf)) <= 0)
+			{
+				res = PQgetResult(conn);
+				pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+			}
+		}
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+	return NULL;
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..73f98a3152 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2293,6 +2293,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 3e199bdfd0..7338c63fe5 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -133,6 +133,7 @@ typedef struct CopyStateData
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -358,8 +359,11 @@ static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
 static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
 							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
 static void EndCopyTo(CopyState cstate);
+static void CopyToStart(CopyState cstate);
+static void CopyToFinish(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
 static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
@@ -586,7 +590,9 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			CopySendChar(cstate, '\0');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1074,7 +1080,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	else
 	{
 		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1814,6 +1820,32 @@ EndCopy(CopyState cstate)
 	pfree(cstate);
 }
 
+static char *buf = NULL;
+static void
+data_dest_cb(void *outbuf, int len)
+{
+	buf = (char *) palloc(len);
+	memcpy(buf, (char *) outbuf, len);
+}
+
+CopyState
+BeginForeignCopyTo(Relation rel)
+{
+	CopyState cstate;
+
+	cstate = BeginCopy(NULL, false, rel, NULL, InvalidOid, NIL, NIL);
+	cstate->copy_dest = COPY_CALLBACK;
+	cstate->data_dest_cb = data_dest_cb;
+	CopyToStart(cstate);
+	return cstate;
+}
+
+void
+EndForeignCopyTo(CopyState cstate)
+{
+	CopyToFinish(cstate);
+}
+
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
@@ -1824,6 +1856,7 @@ BeginCopyTo(ParseState *pstate,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
@@ -1879,6 +1912,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -1949,6 +1987,13 @@ BeginCopyTo(ParseState *pstate,
 	return cstate;
 }
 
+char *
+NextForeignCopyRow(CopyState cstate, TupleTableSlot *slot)
+{
+	CopyOneRowTo(cstate, slot);
+	return buf;
+}
+
 /*
  * This intermediate routine exists mainly to localize the effects of setjmp
  * so we don't need to plaster a lot of variables with "volatile".
@@ -1965,7 +2010,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2004,16 +2051,12 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
- */
-static uint64
-CopyTo(CopyState cstate)
+static void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
@@ -2103,6 +2146,29 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+static void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2134,17 +2200,6 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
@@ -2444,53 +2499,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2800,11 +2866,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2863,14 +2924,13 @@ CopyFrom(CopyState cstate)
 		 */
 		insertMethod = CIM_SINGLE;
 	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
+	else if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
 	{
 		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
 		 *
 		 * Note: It does not matter if any partitions have any volatile
 		 * default expressions as we use the defaults from the target of the
@@ -2910,6 +2970,24 @@ CopyFrom(CopyState cstate)
 								estate, mycid, ti_options);
 	}
 
+	if (insertMethod != CIM_SINGLE)
+		resultRelInfo->UseBulkModifying = true;
+
+	/*
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
+	 */
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->UseBulkModifying &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
+	}
+
 	/*
 	 * If not using batch mode (which allocates slots as needed) set up a
 	 * tuple slot too. When inserting into a partitioned table, we also need
@@ -3033,7 +3111,7 @@ CopyFrom(CopyState cstate)
 				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
 					!has_before_insert_row_trig &&
 					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
+					(resultRelInfo->ri_FdwRoutine == NULL || resultRelInfo->UseBulkModifying);
 
 				/* Set the multi-insert buffer to use for this partition. */
 				if (leafpart_use_multi_insert)
@@ -3292,10 +3370,17 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->UseBulkModifying &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+		target_resultRelInfo->UseBulkModifying = false;
+	}
 
 	/* Tear down the multi-insert buffer data */
 	if (insertMethod != CIM_SINGLE)
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..d3e8f1c720 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1345,6 +1345,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+	resultRelInfo->UseBulkModifying = false;
 }
 
 /*
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index fb6ce49056..c216296811 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -526,6 +526,11 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  rootrel,
 					  estate->es_instrument);
 
+	if (rootResultRelInfo->UseBulkModifying &&
+		leaf_part_rri->ri_FdwRoutine != NULL &&
+		leaf_part_rri->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+		leaf_part_rri->UseBulkModifying = true;
+
 	/*
 	 * Verify result relation is a valid target for an INSERT.  An UPDATE of a
 	 * partition-key becomes a DELETE+INSERT operation, so this check is still
@@ -937,9 +942,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->UseBulkModifying)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1133,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->UseBulkModifying)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..ef119a761a 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -41,4 +42,8 @@ extern uint64 CopyFrom(CopyState cstate);
 
 extern DestReceiver *CreateCopyDestReceiver(void);
 
+extern CopyState BeginForeignCopyTo(Relation rel);
+extern char *NextForeignCopyRow(CopyState cstate, TupleTableSlot *slot);
+extern void EndForeignCopyTo(CopyState cstate);
+
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..073eeee2ca 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef TupleTableSlot *(*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0187989fd1..8ac366a659 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -491,6 +491,14 @@ typedef struct ResultRelInfo
 
 	/* For use by copy.c when performing multi-inserts */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+	/*
+	 * For use by copy.c:
+	 * for partitioned relation "true" means that child relations are allowed for
+	 * using bulk modify operations; for foreign relation (or foreign partition
+	 * of) "true" value means that modify operations must use bulk FDW API.
+	 */
+	bool UseBulkModifying;
 } ResultRelInfo;
 
 /* ----------------
-- 
2.17.1


--------------831941139151C4254FC93B72--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2020-07-09 06:16  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-07-09 06:16 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopyIn
* EndForeignCopyIn
* ExecForeignCopyIn

BeginForeignCopyIn and EndForeignCopyIn initialize and free
the CopyState of bulk COPY. The ExecForeignCopyIn routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++
 doc/src/sgml/fdwhandler.sgml                  |  74 ++++
 src/backend/commands/copy.c                   | 398 +++++++++---------
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/executor/execMain.c               |  53 +++
 src/backend/executor/execPartition.c          |  28 +-
 src/backend/replication/logical/worker.c      |   2 +-
 src/include/commands/copy.h                   |  11 +
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   9 +-
 15 files changed, 669 insertions(+), 218 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 90db550b92..de2638109b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8063,8 +8063,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8075,6 +8076,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8183,6 +8197,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9fc53cad68..19cf119d08 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopyIn(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopyIn = postgresBeginForeignCopyIn;
+	routine->EndForeignCopyIn = postgresEndForeignCopyIn;
+	routine->ExecForeignCopyIn = postgresExecForeignCopyIn;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopyIn
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopyIn(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopyIn
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopyIn(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopyIn
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopyIn(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 83971665e3..aa0b26de77 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2193,6 +2193,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2293,6 +2310,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 74793035d7..e8fd91a7bc 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -795,6 +795,80 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopyIn(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopyIn</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopyIn</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+void
+EndForeignCopyIn(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopyIn(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopyIn</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index db7d24a511..43cd8c011e 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -85,16 +85,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY
  * operation. For simplicity, we use the same struct for all variants of COPY,
@@ -128,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -359,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -595,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1124,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1507,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1542,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1868,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1920,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1930,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2016,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2039,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2055,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2154,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2185,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2495,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2715,12 +2752,11 @@ CopyFrom(CopyState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
+	bool		use_multi_insert;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	uint64		processed = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 
@@ -2820,6 +2856,52 @@ CopyFrom(CopyState cstate)
 		ti_options |= TABLE_INSERT_FROZEN;
 	}
 
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately
+	 * for every tuple. However, there are a number of reasons why we might
+	 * not be able to do this.  We check some conditions below while some
+	 * other target relation properties are left for InitResultRelInfo() to
+	 * check, because they must also be checked for partitions which are
+	 * initialized later.
+	 */
+	if (cstate->volatile_defexprs || list_length(cstate->attnumlist) == 0)
+	{
+		/*
+		 * Can't support bufferization of copy into foreign tables without any
+		 * defined columns or if there are any volatile default expressions in the
+		 * table. Similarly to the trigger case above, such expressions may query
+		 * the table we're inserting into.
+		 *
+		 * Note: It does not matter if any partitions have any volatile
+		 * default expressions as we use the defaults from the target of the
+		 * COPY command.
+		 */
+		use_multi_insert = false;
+	}
+	else if (contain_volatile_functions(cstate->whereClause))
+	{
+		/*
+		 * Can't support multi-inserts if there are any volatile function
+		 * expressions in WHERE clause.  Similarly to the trigger case above,
+		 * such expressions may query the table we're inserting into.
+		 */
+		use_multi_insert = false;
+	}
+	else
+	{
+		/*
+		 * Looks okay to try multi-insert, but that may change once we
+		 * check few more properties in InitResultRelInfo().
+		 *
+		 * For partitioned tables, whether or not to use multi-insert depends
+		 * on the individual parition's properties which are also checked in
+		 * InitResultRelInfo().
+		 */
+		use_multi_insert = true;
+	}
+
 	/*
 	 * We need a ResultRelInfo so we can use the regular executor's
 	 * index-entry-making machinery.  (There used to be a huge amount of code
@@ -2830,6 +2912,7 @@ CopyFrom(CopyState cstate)
 					  cstate->rel,
 					  1,		/* must match rel's position in range_table */
 					  NULL,
+					  use_multi_insert,
 					  0);
 	target_resultRelInfo = resultRelInfo;
 
@@ -2854,11 +2937,6 @@ CopyFrom(CopyState cstate)
 	mtstate->operation = CMD_INSERT;
 	mtstate->resultRelInfo = estate->es_result_relations;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
-
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
 
@@ -2886,82 +2964,23 @@ CopyFrom(CopyState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
+	if (resultRelInfo->ri_usesMultiInsert)
+		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
+								estate, mycid, ti_options);
+
 	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
+	 * Init COPY into foreign table. Initialization of copying into foreign
+	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
 	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
-		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
-								estate, mycid, ti_options);
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate,
+																resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																resultRelInfo);
 	}
 
 	/*
@@ -2970,7 +2989,7 @@ CopyFrom(CopyState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -3013,7 +3032,7 @@ CopyFrom(CopyState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -3021,7 +3040,6 @@ CopyFrom(CopyState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -3080,24 +3098,14 @@ CopyFrom(CopyState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -3149,7 +3157,7 @@ CopyFrom(CopyState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_PartitionInfo->pi_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -3168,9 +3176,6 @@ CopyFrom(CopyState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -3241,7 +3246,7 @@ CopyFrom(CopyState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -3316,11 +3321,8 @@ CopyFrom(CopyState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -3346,14 +3348,19 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	ExecCloseIndices(target_resultRelInfo);
 
@@ -3402,7 +3409,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd989c95e5..2629ceb432 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1786,6 +1786,7 @@ ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
 						  rel,
 						  0,	/* dummy rangetable index */
 						  NULL,
+						  false,
 						  0);
 		resultRelInfo++;
 	}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4fdffad6f3..e08c8b29df 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -851,6 +851,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 							  resultRelation,
 							  resultRelationIndex,
 							  NULL,
+							  false,
 							  estate->es_instrument);
 			resultRelInfo++;
 		}
@@ -883,6 +884,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 								  resultRelDesc,
 								  resultRelIndex,
 								  NULL,
+								  false,
 								  estate->es_instrument);
 				resultRelInfo++;
 			}
@@ -1278,6 +1280,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 				  Relation resultRelationDesc,
 				  Index resultRelationIndex,
 				  Relation partition_root,
+				  bool use_multi_insert,
 				  int instrument_options)
 {
 	List	   *partition_check = NIL;
@@ -1345,6 +1348,55 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionRoot = partition_root;
 	resultRelInfo->ri_PartitionInfo = NULL; /* may be set later */
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
+
+	/*
+	 * If the caller has asked to use "multi-insert" mode, check if the
+	 * relation allows it and if it does set ri_usesMultiInsert to true.
+	 */
+	if (!use_multi_insert)
+	{
+		/* Caller didn't ask for it. */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_TrigDesc != NULL &&
+			 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+	{
+		/*
+		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+		 * triggers on the table. Such triggers might query the table we're
+		 * inserting into and act differently if the tuples that have already
+		 * been processed and prepared for insertion are not there.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			 resultRelInfo->ri_TrigDesc != NULL &&
+			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
+	{
+		/*
+		 * For partitioned tables we can't support multi-inserts when there
+		 * are any statement level insert triggers. It might be possible to
+		 * allow partitioned tables with such triggers in the future, but for
+		 * now, CopyMultiInsertInfoFlush expects that any before row insert
+		 * and statement level insert triggers are on the same relation.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else if (resultRelInfo->ri_FdwRoutine != NULL &&
+			 resultRelInfo->ri_FdwRoutine->ExecForeignCopyIn == NULL)
+	{
+		/*
+		 * For a foreign table, we can't support multi-inserts unless its FDW
+		 * provides the necessary COPY interface.
+		 */
+		resultRelInfo->ri_usesMultiInsert = false;
+	}
+	else
+	{
+		/* OK, caller can use multi-insert on this relation. */
+		resultRelInfo->ri_usesMultiInsert = true;
+	}
 }
 
 /*
@@ -1434,6 +1486,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
 					  rel,
 					  0,		/* dummy rangetable index */
 					  NULL,
+					  false,
 					  estate->es_instrument);
 	estate->es_trig_target_relations =
 		lappend(estate->es_trig_target_relations, rInfo);
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 79fcbd6b06..7b72a09fb7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -524,6 +524,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  partrel,
 					  node ? node->rootRelation : 1,
 					  rootrel,
+					  rootResultRelInfo->ri_usesMultiInsert,
 					  estate->es_instrument);
 
 	/*
@@ -937,9 +938,14 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert &&
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopyIn(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1121,10 +1127,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopyIn != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopyIn(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index b576e342cb..9f9cf2dbdb 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -211,7 +211,7 @@ create_estate_for_relation(LogicalRepRelMapEntry *rel)
 	ExecInitRangeTable(estate, list_make1(rte));
 
 	resultRelInfo = makeNode(ResultRelInfo);
-	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, 0);
+	InitResultRelInfo(resultRelInfo, rel->localrel, 1, NULL, false, 0);
 
 	estate->es_result_relations = resultRelInfo;
 	estate->es_num_result_relations = 1;
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 415e117407..72612bd5a6 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -189,6 +189,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Relation resultRelationDesc,
 							  Index resultRelationIndex,
 							  Relation partition_root,
+							  bool use_multi_insert,
 							  int instrument_options);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecCleanUpTriggerState(EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..11ea451fe4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopyIn_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopyIn_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopyIn_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopyIn_function BeginForeignCopyIn;
+	EndForeignCopyIn_function EndForeignCopyIn;
+	ExecForeignCopyIn_function ExecForeignCopyIn;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0b42dd6f94..89ae9afaa4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -489,7 +489,14 @@ typedef struct ResultRelInfo
 	/* Additional information specific to partition tuple routing */
 	struct PartitionRoutingInfo *ri_PartitionInfo;
 
-	/* For use by copy.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copy.c.
+	 *
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.1


--------------2563DFF8F134DCE2289A1427--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/4] Fast COPY FROM into the foreign or sharded table.
@ 2020-09-10 09:21  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-09-10 09:21 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion: https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +++-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++
 doc/src/sgml/fdwhandler.sgml                  |  75 ++++++
 src/backend/commands/copy.c                   | 225 +++++++++++-------
 src/backend/executor/execPartition.c          |  34 ++-
 src/include/commands/copy.h                   |  11 +
 src/include/foreign/fdwapi.h                  |  15 ++
 10 files changed, 549 insertions(+), 106 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ad37a74221..a37981ff66 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 84bc0ee381..5206814f10 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8084,8 +8084,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8096,6 +8097,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8204,6 +8218,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index a31abce7c9..9685e731e0 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2050,6 +2062,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index d452d06343..1a56432f0f 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2213,6 +2213,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2313,6 +2330,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 72fa127212..81728945ea 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,81 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 2119db4213..02a034fb37 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -118,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -349,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -585,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1114,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1497,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1532,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1858,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1910,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1920,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2006,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2029,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2045,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2144,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2175,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2485,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2895,13 +2942,18 @@ CopyFrom(CopyState cstate)
 	mtstate->resultRelInfo = estate->es_result_relations;
 
 	/*
-	 * Init COPY into foreign table. Initialization of copying into foreign
-	 * partitions will be done later.
+	 * Init COPY into foreign table.
 	 */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-																resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
+
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -3299,10 +3351,16 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
 														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
@@ -3354,7 +3412,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index baaa0f61fa..581498cf6c 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -580,8 +580,12 @@ checkMultiInsertMode(const ResultRelInfo *rri, const ResultRelInfo *parent)
 		 */
 		return false;
 
-	if (rri->ri_FdwRoutine != NULL)
-		/* Foreign tables don't support multi-inserts. */
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
@@ -1041,9 +1045,13 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1245,10 +1253,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..e932bdf2f4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopy_function BeginForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------D1E70C405CAC2D2D8C75C1DF
Content-Type: text/x-patch; charset=UTF-8;
 name="v9-0003-Add-separated-connections-into-the-postgres_fdw.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v9-0003-Add-separated-connections-into-the-postgres_fdw.patc";
 filename*1="h"



^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2020-09-20 08:44  Andrey V. Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey V. Lepikhov @ 2020-09-20 08:44 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +++-
 contrib/postgres_fdw/postgres_fdw.c           | 143 +++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++
 doc/src/sgml/fdwhandler.sgml                  |  75 ++++++
 src/backend/commands/copy.c                   | 228 +++++++++++-------
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  26 +-
 src/include/commands/copy.h                   |  11 +
 src/include/foreign/fdwapi.h                  |  15 ++
 11 files changed, 552 insertions(+), 106 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 2d44df19fe..fa7740163d 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 10e23d02ed..26d989591d 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index a31abce7c9..9685e731e0 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -533,6 +542,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2050,6 +2062,137 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+	{
+		PGresult *res = PQgetResult(conn);
+
+		pgfdw_report_error(ERROR, res, conn, true, copy_fmstate->query);
+	}
+}
+
+/*
+ *
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool status = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		status = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, status ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			ereport(ERROR,
+					(errmsg("error returned by PQputCopyEnd: %s",
+							PQerrorMessage(conn))));
+
+		/* After successfully  sending an EOF signal, check command status. */
+		res = PQgetResult(conn);
+		if ((!status && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(status && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!status)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 78156d10b4..45e5e2042c 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 72fa127212..81728945ea 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,81 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+TupleTableSlot *
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> cis a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, attempts to insert into the foreign table will fail
+     with an error message.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 5a36a86c60..4deee7ffc3 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -118,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -349,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -585,7 +583,8 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			CopySendChar(cstate, '\n');
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1114,8 +1113,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1497,6 +1496,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1532,6 +1532,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1858,20 +1863,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1910,8 +1920,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1920,6 +1931,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2006,7 +2022,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2029,7 +2047,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2045,19 +2063,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2144,6 +2165,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2175,24 +2222,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2485,53 +2521,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(buffer->slots[i], estate, false, NULL,
+										  NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2839,8 +2886,11 @@ CopyFrom(CopyState cstate)
 	 * checked by calling ExecRelationAllowsMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause) &&
 		ExecRelationAllowsMultiInsert(target_resultRelInfo, NULL))
 		target_resultRelInfo->ri_usesMultiInsert = true;
@@ -2868,12 +2918,17 @@ CopyFrom(CopyState cstate)
 
 	/*
 	 * Init COPY into foreign table. Initialization of copying into foreign
-	 * partitions will be done later.
+-	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -3271,10 +3326,16 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+ 														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
@@ -3326,7 +3387,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 97a483b179..1397e77197 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1372,8 +1372,12 @@ ExecRelationAllowsMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 121484374f..c56b0000b8 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -1001,9 +1001,13 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1205,10 +1209,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..e932bdf2f4 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+											 ResultRelInfo *rinfo);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+													   TupleTableSlot **slots,
+													   int nslots);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* COPY a bulk of tuples into a foreign relation */
+	BeginForeignCopy_function BeginForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.17.1


--------------238E430336C30311DCAA1E4F
Content-Type: text/x-patch; charset=UTF-8;
 name="v10-0001-Move-multi-insert-decision-logic-into-executor.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v10-0001-Move-multi-insert-decision-logic-into-executor.patc";
 filename*1="h"



^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2020-10-19 11:24  Andrey V. Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey V. Lepikhov @ 2020-10-19 11:24 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +++-
 contrib/postgres_fdw/postgres_fdw.c           | 137 ++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++
 src/backend/commands/copy.c                   | 236 +++++++++++-------
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  29 ++-
 src/include/commands/copy.h                   |  11 +
 src/include/foreign/fdwapi.h                  |  15 ++
 11 files changed, 554 insertions(+), 107 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 2d44df19fe..fa7740163d 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 2d88d06358..be8db5ac63 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9c5aaacc51..1657a20d9b 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -534,6 +543,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,131 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ *
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!OK)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 7581c5417b..22dcd12f02 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 83ce196a45..26d79ad051 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -118,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -349,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -585,7 +583,13 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			Assert(!cstate->binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1114,8 +1118,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1501,6 +1505,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1536,6 +1541,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1862,20 +1872,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1914,8 +1929,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1924,6 +1940,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2001,7 +2022,7 @@ BeginCopyTo(ParseState *pstate,
 static uint64
 DoCopyTo(CopyState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -2010,7 +2031,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2033,7 +2056,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2049,19 +2072,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2148,6 +2174,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2179,24 +2231,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2486,54 +2527,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2838,8 +2888,11 @@ CopyFrom(CopyState cstate)
 	 * checked by calling ExecRelationAllowsMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecRelationAllowsMultiInsert(target_resultRelInfo, NULL);
@@ -2863,10 +2916,18 @@ CopyFrom(CopyState cstate)
 	 * Init COPY into foreign table. Initialization of copying into foreign
 	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -3261,10 +3322,16 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
@@ -3315,7 +3382,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 0ad98ff0e7..9d465cebaf 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1291,8 +1291,12 @@ ExecRelationAllowsMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 121484374f..fae21356c7 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -1001,9 +1001,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_PartitionInfo = partrouteinfo;
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
@@ -1205,10 +1212,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..52b213f5aa 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.17.1


--------------545C3421673CBF7E5637A5E1
Content-Type: text/x-patch; charset=UTF-8;
 name="v11-0001-Move-multi-insert-decision-logic-into-executor.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="v11-0001-Move-multi-insert-decision-logic-into-executor.patc";
 filename*1="h"



^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2020-11-10 17:55  Andrey V. Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey V. Lepikhov @ 2020-11-10 17:55 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +++-
 contrib/postgres_fdw/postgres_fdw.c           | 137 ++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++
 src/backend/commands/copy.c                   | 236 +++++++++++-------
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  29 ++-
 src/include/commands/copy.h                   |  11 +
 src/include/foreign/fdwapi.h                  |  15 ++
 11 files changed, 554 insertions(+), 107 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 2d44df19fe..fa7740163d 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1758,6 +1760,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2061,6 +2077,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2069,10 +2109,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2081,6 +2119,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2100,18 +2141,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 2d88d06358..be8db5ac63 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 9c5aaacc51..1657a20d9b 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -190,6 +191,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -356,6 +358,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -534,6 +543,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2051,6 +2063,131 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ *
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!OK)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 7581c5417b..22dcd12f02 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index d882396d6f..21f7613dfe 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -118,11 +118,14 @@ typedef struct CopyStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to or from */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDIN/STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 	copy_data_source_cb data_source_cb; /* function for reading data */
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	bool		binary;			/* binary format? */
 	bool		freeze;			/* freeze rows on loading? */
 	bool		csv_mode;		/* Comma Separated Value format? */
@@ -349,17 +352,12 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 
 /* non-export function prototypes */
 static CopyState BeginCopy(ParseState *pstate, bool is_from, Relation rel,
-						   RawStmt *raw_query, Oid queryRelId, List *attnamelist,
-						   List *options);
+						   TupleDesc srcTupDesc, RawStmt *raw_query,
+						   Oid queryRelId, List *attnamelist, List *options);
 static void EndCopy(CopyState cstate);
 static void ClosePipeToProgram(CopyState cstate);
-static CopyState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
-							 Oid queryRelId, const char *filename, bool is_program,
-							 List *attnamelist, List *options);
-static void EndCopyTo(CopyState cstate);
 static uint64 DoCopyTo(CopyState cstate);
 static uint64 CopyTo(CopyState cstate);
-static void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
 static bool CopyReadLine(CopyState cstate);
 static bool CopyReadLineText(CopyState cstate);
 static int	CopyReadAttributesText(CopyState cstate);
@@ -585,7 +583,13 @@ CopySendEndOfRow(CopyState cstate)
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
 		case COPY_CALLBACK:
-			Assert(false);		/* Not yet supported. */
+			Assert(!cstate->binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 			break;
 	}
 
@@ -1114,8 +1118,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	}
 	else
 	{
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
@@ -1501,6 +1505,7 @@ static CopyState
 BeginCopy(ParseState *pstate,
 		  bool is_from,
 		  Relation rel,
+		  TupleDesc srcTupDesc,
 		  RawStmt *raw_query,
 		  Oid queryRelId,
 		  List *attnamelist,
@@ -1536,6 +1541,11 @@ BeginCopy(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -1862,20 +1872,25 @@ EndCopy(CopyState cstate)
 /*
  * Setup CopyState to read tuples from a table or a query for COPY TO.
  */
-static CopyState
+CopyState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc tupDesc,
 			RawStmt *query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || tupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -1914,8 +1929,9 @@ BeginCopyTo(ParseState *pstate,
 							RelationGetRelationName(rel))));
 	}
 
-	cstate = BeginCopy(pstate, false, rel, query, queryRelId, attnamelist,
-					   options);
+	cstate = BeginCopy(pstate, false, rel, tupDesc, query, queryRelId,
+					   attnamelist, options);
+
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	if (pipe)
@@ -1924,6 +1940,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -2001,7 +2022,7 @@ BeginCopyTo(ParseState *pstate,
 static uint64
 DoCopyTo(CopyState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -2010,7 +2031,9 @@ DoCopyTo(CopyState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -2033,7 +2056,7 @@ DoCopyTo(CopyState cstate)
 /*
  * Clean up storage and release resources for COPY TO.
  */
-static void
+void
 EndCopyTo(CopyState cstate)
 {
 	if (cstate->queryDesc != NULL)
@@ -2049,19 +2072,22 @@ EndCopyTo(CopyState cstate)
 	EndCopy(cstate);
 }
 
-/*
- * Copy from relation or query TO file.
+/* Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyState cstate)
+void
+CopyToStart(CopyState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -2148,6 +2174,32 @@ CopyTo(CopyState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyState cstate)
+{
+	if (cstate->binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -2179,24 +2231,13 @@ CopyTo(CopyState cstate)
 		ExecutorRun(cstate->queryDesc, ForwardScanDirection, 0L, true);
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
-
-	if (cstate->binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
@@ -2486,54 +2527,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
 
-		ExecClearTuple(slots[i]);
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
+
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -2838,8 +2888,11 @@ CopyFrom(CopyState cstate)
 	 * checked by calling ExecRelationAllowsMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecRelationAllowsMultiInsert(target_resultRelInfo, NULL);
@@ -2863,10 +2916,18 @@ CopyFrom(CopyState cstate)
 	 * Init COPY into foreign table. Initialization of copying into foreign
 	 * partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -3244,10 +3305,16 @@ CopyFrom(CopyState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
@@ -3298,7 +3365,8 @@ BeginCopyFrom(ParseState *pstate,
 	MemoryContext oldcontext;
 	bool		volatile_defexprs;
 
-	cstate = BeginCopy(pstate, true, rel, NULL, InvalidOid, attnamelist, options);
+	cstate = BeginCopy(pstate, true, rel, NULL, NULL, InvalidOid, attnamelist,
+																	options);
 	oldcontext = MemoryContextSwitchTo(cstate->copycontext);
 
 	/* Initialize state variables */
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 0c728315fa..cc758cd03a 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecRelationAllowsMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 2ce4afc9ad..49812e9a9d 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -997,9 +997,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1200,10 +1207,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				Assert(resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL);
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index c639833565..08309149ea 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -22,6 +22,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -39,6 +40,16 @@ extern void CopyFromErrorCallback(void *arg);
 
 extern uint64 CopyFrom(CopyState cstate);
 
+extern CopyState BeginCopyTo(ParseState *pstate, Relation rel,
+							 TupleDesc tupDesc, RawStmt *query,
+							 Oid queryRelId, const char *filename, bool is_program,
+							 copy_data_dest_cb data_dest_cb, List *attnamelist,
+							 List *options);
+extern void EndCopyTo(CopyState cstate);
+extern void CopyOneRowTo(CopyState cstate, TupleTableSlot *slot);
+extern void CopyToStart(CopyState cstate);
+extern void CopyToFinish(CopyState cstate);
+
 extern DestReceiver *CreateCopyDestReceiver(void);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..52b213f5aa 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.26.2


--------------2D6A3127D85FEDD1D16D6C49--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2020-12-14 08:37  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-12-14 08:37 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +++++-
 contrib/postgres_fdw/postgres_fdw.c           | 137 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 133 +++++++++--------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 540 insertions(+), 101 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ca2f9f3215..b2a71faabc 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 2d88d06358..0e2c15c648 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2: ""
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3: ""
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index b6c72e1d1e..dd185bdc3b 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,131 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ *
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!OK)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 7581c5417b..22dcd12f02 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index b6143b8bf2..32cff00762 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 6d4f6cb80d..73fc838625 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -307,61 +307,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	ResultRelInfo *resultRelInfo = buffer->resultRelInfo;
 	TupleTableSlot **slots = buffer->slots;
 
-	/*
-	 * Print error context information correctly, if one of the operations
-	 * below fail.
-	 */
-	cstate->line_buf_valid = false;
-	save_cur_lineno = cstate->cur_lineno;
-
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -666,8 +668,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -691,10 +696,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1072,10 +1085,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index c7e5f04446..b1d50b01cc 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -50,6 +50,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -80,11 +81,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -114,7 +118,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -286,6 +289,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	resetStringInfo(fe_msgbuf);
@@ -373,19 +384,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -450,6 +466,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query && !is_from);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -695,6 +716,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -772,7 +798,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -781,7 +807,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -821,18 +849,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -919,6 +951,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -951,23 +1009,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 9809c03a8e..a21d4d2fc1 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 5a201dfbfa..56ec9bbf41 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -997,9 +997,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1200,10 +1207,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 127a3c61e2..01bb3e8ad4 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..52b213f5aa 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------864DF32B410D06E611DA27E2--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2020-12-14 08:37  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2020-12-14 08:37 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 +++++-
 contrib/postgres_fdw/postgres_fdw.c           | 137 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 +++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 540 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ca2f9f3215..b2a71faabc 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 2d88d06358..be8db5ac63 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index b6c72e1d1e..dd185bdc3b 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,131 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ *
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ *
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	Assert(copy_fmstate == NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		copy_fmstate = NULL; /* Detect problems */
+
+		/* Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+
+		if (!OK)
+			PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index eef410db39..8fc5ff018f 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 7581c5417b..22dcd12f02 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index b6143b8bf2..32cff00762 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 6d4f6cb80d..17aac24bdd 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -314,54 +314,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -666,8 +675,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -691,10 +703,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1072,10 +1092,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index c7e5f04446..608bb3771d 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -50,6 +50,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -80,11 +81,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -114,7 +118,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -286,6 +289,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	resetStringInfo(fe_msgbuf);
@@ -373,19 +384,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -450,6 +466,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -695,6 +716,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -772,7 +798,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -781,7 +807,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -821,18 +849,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -919,6 +951,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -951,23 +1009,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 9809c03a8e..a21d4d2fc1 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 637e900b09..f3b9197db1 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 127a3c61e2..01bb3e8ad4 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 95556dfb15..52b213f5aa 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------F015C28C1D394891849B3B51--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH 2/2] Fast COPY FROM into the foreign or sharded table.
@ 2021-01-12 03:54  Andrey Lepikhov <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andrey Lepikhov @ 2021-01-12 03:54 UTC (permalink / raw)

This feature enables bulk COPY into foreign table in the case of
multi inserts is possible and foreign table has non-zero number of columns.

FDWAPI was extended by next routines:
* BeginForeignCopy
* EndForeignCopy
* ExecForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine send
'COPY ... FROM STDIN' command to the foreign server, in iterative
manner send tuples by CopyTo() machinery, send EOF to this connection.

Code that constructed list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is separated to the deparseRelColumnList().
It is reused in the deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used for send text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. ALso for this
reason CopyTo() routine was split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

Enum CopyInsertMethod removed. This logic implements by ri_usesMultiInsert
field of the ResultRelInfo sructure.

Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote
---
 contrib/postgres_fdw/deparse.c                |  60 ++++++--
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++++++-
 contrib/postgres_fdw/postgres_fdw.c           | 130 ++++++++++++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 ++++++
 doc/src/sgml/fdwhandler.sgml                  |  73 ++++++++++
 src/backend/commands/copy.c                   |   4 +-
 src/backend/commands/copyfrom.c               | 126 ++++++++++-------
 src/backend/commands/copyto.c                 |  84 ++++++++---
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execPartition.c          |  27 +++-
 src/include/commands/copy.h                   |   8 +-
 src/include/foreign/fdwapi.h                  |  15 ++
 13 files changed, 533 insertions(+), 94 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 3cf7b4eb1e..b1ca479a65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -184,6 +184,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1763,6 +1765,20 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse COPY FROM into given buf.
+ * We need to use list of parameters at each query.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2066,6 +2082,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2074,10 +2114,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2086,6 +2124,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2105,18 +2146,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c11092f8cc..db7b09c1fe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8076,8 +8076,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8088,6 +8089,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8196,6 +8210,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 2f2d4d171c..fa0eccb485 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -191,6 +192,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -357,6 +359,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(ModifyTableState *mtstate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -535,6 +544,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2052,6 +2064,124 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin an COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(ModifyTableState *mtstate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, mtstate->ps.state);
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, NULL, RelationGetDescr(rel), NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish an COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : _("canceled by server")) <= 0 ||
+			PQflush(conn))
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if ((!OK && PQresultStatus(res) != PGRES_FATAL_ERROR) ||
+			(OK && PQresultStatus(res) != PGRES_COMMAND_OK))
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we've pumped libpq back to idle state */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 19ea27a1bc..c38c219adf 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -162,6 +162,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 							 List *targetAttrs, bool doNothing,
 							 List *withCheckOptionList, List *returningList,
 							 List **retrieved_attrs);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 25dbc08b98..53b9d865da 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2212,6 +2212,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2312,6 +2329,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 9c9293414c..a9a7402440 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -796,6 +796,79 @@ EndForeignInsert(EState *estate,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(ModifyTableState *mtstate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing an copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a bulk of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>mtstate</literal> is the overall state of the
+     <structname>ModifyTable</structname> plan node being executed; global data about
+     the plan and execution state is available via this structure.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     When this is called by a <command>COPY FROM</command> command, the
+     plan-related global data in <literal>mtstate</literal> is not provided.
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a bulk of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is a number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
 int
 IsForeignRelUpdatable(Relation rel);
 </programlisting>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8c712c8737..cd8aa57026 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -303,8 +303,8 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	{
 		CopyToState cstate;
 
-		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+		cstate = BeginCopyTo(pstate, rel, NULL, query, relid,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 4e2320e2fa..57e4addabf 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -316,54 +316,63 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, NULL,
-									  NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo, buffer->slots[i],
+										  estate, false, NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -668,8 +677,11 @@ CopyFrom(CopyFromState cstate)
 	 * checked by calling ExecSetRelationUsesMultiInsert().  It does not matter
 	 * whether partitions have any volatile default expressions as we use the
 	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
 	 */
 	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
 		!contain_volatile_functions(cstate->whereClause))
 		target_resultRelInfo->ri_usesMultiInsert =
 					ExecSetRelationUsesMultiInsert(target_resultRelInfo, NULL);
@@ -693,10 +705,18 @@ CopyFrom(CopyFromState cstate)
 	 * Init copying process into foreign table. Initialization of copying into
 	 * foreign partitions will be done later.
 	 */
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			Assert(target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate,
+																  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -1075,10 +1095,16 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert &&
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+														target_resultRelInfo);
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
 	CopyMultiInsertInfoCleanup(&multiInsertInfo);
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index e04ec1e331..7a10c9dc9e 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -52,6 +52,7 @@ typedef enum CopyDest
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_OLD_FE,				/* to frontend (2.0 protocol) */
 	COPY_NEW_FE,				/* to frontend (3.0 protocol) */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -82,11 +83,14 @@ typedef struct CopyToStateData
 
 	/* parameters from the COPY command */
 	Relation	rel;			/* relation to copy to */
+	TupleDesc	tupDesc;		/* COPY TO will be used for manual tuple copying
+								  * into the destination */
 	QueryDesc  *queryDesc;		/* executable query to copy from */
 	List	   *attnumlist;		/* integer list of attnums to copy */
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -117,7 +121,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
 static uint64 CopyTo(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -289,6 +292,14 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
 	}
 
 	/* Update the progress */
@@ -382,19 +393,24 @@ EndCopy(CopyToState cstate)
 CopyToState
 BeginCopyTo(ParseState *pstate,
 			Relation rel,
+			TupleDesc srcTupDesc,
 			RawStmt *raw_query,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
 
+	/* Impossible to mix CopyTo modes */
+	Assert(rel == NULL || srcTupDesc == NULL);
+
 	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
@@ -459,6 +475,11 @@ BeginCopyTo(ParseState *pstate,
 
 		tupDesc = RelationGetDescr(cstate->rel);
 	}
+	else if (srcTupDesc)
+	{
+		Assert(!raw_query);
+		tupDesc = cstate->tupDesc = srcTupDesc;
+	}
 	else
 	{
 		List	   *rewritten;
@@ -704,6 +725,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -786,7 +812,7 @@ BeginCopyTo(ParseState *pstate,
 uint64
 DoCopyTo(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
 	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	uint64		processed;
 
@@ -795,7 +821,9 @@ DoCopyTo(CopyToState cstate)
 		if (fe_copy)
 			SendCopyBegin(cstate);
 
+		CopyToStart(cstate);
 		processed = CopyTo(cstate);
+		CopyToFinish(cstate);
 
 		if (fe_copy)
 			SendCopyEnd(cstate);
@@ -835,18 +863,22 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separated to the routine to prevent duplicate operations in the case of
+ * manual mode, where tuples are copied to the destination one by one, by call of
+ * the CopyOneRowTo() routine.
  */
-static uint64
-CopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
+	else if (cstate->tupDesc)
+		tupDesc = cstate->tupDesc;
 	else
 		tupDesc = cstate->queryDesc->tupDesc;
 	num_phys_attrs = tupDesc->natts;
@@ -933,6 +965,32 @@ CopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+static uint64
+CopyTo(CopyToState cstate)
+{
+	uint64		processed;
 
 	if (cstate->rel)
 	{
@@ -967,23 +1025,13 @@ CopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
-
 	return processed;
 }
 
 /*
  * Emit one row during CopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f217486b85..fcfd6027cc 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1294,8 +1294,12 @@ ExecSetRelationUsesMultiInsert(const ResultRelInfo *rri,
 		rri->ri_TrigDesc->trig_insert_new_table)
 		return false;
 
-	/* Foreign tables don't support multi-inserts. */
-	if (rri->ri_FdwRoutine != NULL)
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
 		return false;
 
 	/* OK, caller can use multi-insert on this relation. */
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 1f5f392bf9..386a2a9013 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -996,9 +996,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			Assert(partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL);
+			partRelInfo->ri_FdwRoutine->BeginForeignCopy(mtstate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	partRelInfo->ri_CopyMultiInsertBuffer = NULL;
 
@@ -1199,10 +1206,16 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert &&
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+															   resultRelInfo);
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Check if this result rel is one belonging to the node's subplans,
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..a7e7224ac8 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -78,12 +79,17 @@ extern DestReceiver *CreateCopyDestReceiver(void);
 /*
  * internal prototypes
  */
-extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
+extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel,
+							   TupleDesc tupDesc, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 2953499fb1..38e5dbb8e2 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -104,6 +104,16 @@ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
 typedef void (*EndForeignInsert_function) (EState *estate,
 										   ResultRelInfo *rinfo);
 
+typedef void (*BeginForeignCopy_function) (ModifyTableState *mtstate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
@@ -220,6 +230,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
-- 
2.25.1


--------------5FF89B363D34DDCF74284633--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* [PATCH] Fast COPY FROM into the foreign or sharded table.
@ 2021-02-09 03:50  Takayuki Tsunakawa <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Takayuki Tsunakawa @ 2021-02-09 03:50 UTC (permalink / raw)

This feature enables bulk COPY into foreign table when multi-insert
is possible and foreign table has non-zero number of columns.

The following routines are added to the FDW interface:
* BeginForeignCopy
* ExecForeignCopy
* EndForeignCopy

BeginForeignCopy and EndForeignCopy initialize and free
the CopyState of bulk COPY. The ExecForeignCopy routine runs
'COPY ... FROM STDIN' command to the foreign server, in an iterative
manner to send tuples using the CopyTo() machinery.

Code that constructs a list of columns for a given foreign relation
in the deparseAnalyzeSql() routine is split into deparseRelColumnList().
It is reused in deparseCopyFromSql().

Added TAP-tests on the specific corner cases of COPY FROM STDIN operation.

By the analogy of CopyFrom() the CopyState structure was extended
with data_dest_cb callback. It is used to send the text representation
of a tuple to a custom destination.
The PgFdwModifyState structure is extended with the cstate field.
It is needed for avoid repeated initialization of CopyState. Also for this
reason CopyTo() routine is split into the set of routines CopyToStart()/
CopyTo()/CopyToFinish().

When 0d5f05cde introduced support for using multi-insert mode when
copying into partitioned tables, it introduced single variable of
enum type CopyInsertMethod shared across all potential target
relations (partitions) that, along with some target relation
properties, dictated whether to engage multi-insert mode for a given
target relation.

Change that decision logic to the combination of ExecMultiInsertAllowed()
and its caller. The former encapsulates the common criteria to allow
multi-insert. The latter uses additional criteria and sets the new
boolean field ri_usesMultiInsert of ResultRelInfo.
That prevents repeated computation of the same information in some cases,
especially for partitions, and the new arrangement results in slightly
more readability.
Enum CopyInsertMethod is removed.

Authors: Andrey Lepikhov, Ashutosh Bapat, Amit Langote, Takayuki Tsunakawa
Reviewed-by: Ashutosh Bapat, Amit Langote, Takayuki Tsunakawa
Discussion:
https://www.postgresql.org/message-id/flat/3d0909dc-3691-a576-208a-90986e55489f%40postgrespro.ru
---
 contrib/postgres_fdw/deparse.c                |  63 +++-
 .../postgres_fdw/expected/postgres_fdw.out    |  46 ++-
 contrib/postgres_fdw/postgres_fdw.c           | 141 +++++++++
 contrib/postgres_fdw/postgres_fdw.h           |   1 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  45 +++
 doc/src/sgml/fdwhandler.sgml                  |  71 ++++-
 src/backend/commands/copy.c                   |   2 +-
 src/backend/commands/copyfrom.c               | 271 ++++++++----------
 src/backend/commands/copyto.c                 |  88 ++++--
 src/backend/executor/execMain.c               |  44 +++
 src/backend/executor/execPartition.c          |  37 ++-
 src/include/commands/copy.h                   |   5 +
 src/include/commands/copyfrom_internal.h      |  10 -
 src/include/executor/executor.h               |   1 +
 src/include/foreign/fdwapi.h                  |  15 +
 src/include/nodes/execnodes.h                 |   8 +-
 16 files changed, 637 insertions(+), 211 deletions(-)

diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index bdc4c3620d..bf93c1d091 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -185,6 +185,8 @@ static void appendAggOrderBy(List *orderList, List *targetList,
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist, bool force_colno,
 									deparse_expr_cxt *context);
+static List *deparseRelColumnList(StringInfo buf, Relation rel,
+								  bool enclose_in_parens);
 
 /*
  * Helper functions
@@ -1859,6 +1861,23 @@ deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 						 withCheckOptionList, returningList, retrieved_attrs);
 }
 
+/*
+ * Deparse remote COPY FROM statement
+ *
+ * Note that this explicitly specifies the list of COPY's target columns
+ * to account for the fact that the remote table's columns may not match
+ * exactly with the columns declared in the local definition.
+ */
+void
+deparseCopyFromSql(StringInfo buf, Relation rel)
+{
+	appendStringInfoString(buf, "COPY ");
+	deparseRelation(buf, rel);
+	(void) deparseRelColumnList(buf, rel, true);
+
+	appendStringInfoString(buf, " FROM STDIN ");
+}
+
 /*
  * deparse remote UPDATE statement
  *
@@ -2120,6 +2139,30 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
  */
 void
 deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
+{
+	appendStringInfoString(buf, "SELECT ");
+	*retrieved_attrs = deparseRelColumnList(buf, rel, false);
+
+	/* Don't generate bad syntax for zero-column relation. */
+	if (list_length(*retrieved_attrs) == 0)
+		appendStringInfoString(buf, "NULL");
+
+	/*
+	 * Construct FROM clause
+	 */
+	appendStringInfoString(buf, " FROM ");
+	deparseRelation(buf, rel);
+}
+
+/*
+ * Construct the list of columns of given foreign relation in the order they
+ * appear in the tuple descriptor of the relation. Ignore any dropped columns.
+ * Use column names on the foreign server instead of local names.
+ *
+ * Optionally enclose the list in parantheses.
+ */
+static List *
+deparseRelColumnList(StringInfo buf, Relation rel, bool enclose_in_parens)
 {
 	Oid			relid = RelationGetRelid(rel);
 	TupleDesc	tupdesc = RelationGetDescr(rel);
@@ -2128,10 +2171,8 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 	List	   *options;
 	ListCell   *lc;
 	bool		first = true;
+	List	   *retrieved_attrs = NIL;
 
-	*retrieved_attrs = NIL;
-
-	appendStringInfoString(buf, "SELECT ");
 	for (i = 0; i < tupdesc->natts; i++)
 	{
 		/* Ignore dropped columns. */
@@ -2140,6 +2181,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		if (!first)
 			appendStringInfoString(buf, ", ");
+		else if (enclose_in_parens)
+			appendStringInfoChar(buf, '(');
+
 		first = false;
 
 		/* Use attribute name or column_name option. */
@@ -2159,18 +2203,13 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
 
 		appendStringInfoString(buf, quote_identifier(colname));
 
-		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+		retrieved_attrs = lappend_int(retrieved_attrs, i + 1);
 	}
 
-	/* Don't generate bad syntax for zero-column relation. */
-	if (first)
-		appendStringInfoString(buf, "NULL");
+	if (enclose_in_parens && list_length(retrieved_attrs) > 0)
+		appendStringInfoChar(buf, ')');
 
-	/*
-	 * Construct FROM clause
-	 */
-	appendStringInfoString(buf, " FROM ");
-	deparseRelation(buf, rel);
+	return retrieved_attrs;
 }
 
 /*
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 7f69fa0054..b214395a78 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8078,8 +8078,9 @@ copy rem2 from stdin;
 copy rem2 from stdin; -- ERROR
 ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
 DETAIL:  Failing row contains (-1, xyzzy).
-CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
-COPY rem2, line 1: "-1	xyzzy"
+CONTEXT:  COPY loc2, line 1: "-1	xyzzy"
+remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 2
 select * from rem2;
  f1 | f2  
 ----+-----
@@ -8090,6 +8091,19 @@ select * from rem2;
 alter foreign table rem2 drop constraint rem2_f1positive;
 alter table loc2 drop constraint loc2_f1positive;
 delete from rem2;
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+copy foo from stdin;
+NOTICE:  (1)
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -8198,6 +8212,34 @@ drop trigger rem2_trig_row_before on rem2;
 drop trigger rem2_trig_row_after on rem2;
 drop trigger loc2_trig_row_before_insert on loc2;
 delete from rem2;
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+ERROR:  column "f1" of relation "loc2" does not exist
+CONTEXT:  remote SQL command: COPY public.loc2(f1, f2) FROM STDIN 
+COPY rem2, line 3
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+ f1 | f2 
+----+----
+(0 rows)
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(2 rows)
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+select * from rem2;
+--
+(4 rows)
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index c590f374c6..c615cafd8f 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -18,6 +18,7 @@
 #include "access/sysattr.h"
 #include "access/table.h"
 #include "catalog/pg_class.h"
+#include "commands/copy.h"
 #include "commands/defrem.h"
 #include "commands/explain.h"
 #include "commands/vacuum.h"
@@ -209,6 +210,7 @@ typedef struct PgFdwModifyState
 	/* for update row movement if subplan result rel */
 	struct PgFdwModifyState *aux_fmstate;	/* foreign-insert state, if
 											 * created */
+	CopyToState cstate; /* foreign COPY state, if used */
 } PgFdwModifyState;
 
 /*
@@ -383,6 +385,13 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 									   ResultRelInfo *resultRelInfo);
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
+static void postgresBeginForeignCopy(EState *estate,
+									   ResultRelInfo *resultRelInfo);
+static void postgresEndForeignCopy(EState *estate,
+									 ResultRelInfo *resultRelInfo);
+static void postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+									  TupleTableSlot **slots,
+									  int nslots);
 static int	postgresIsForeignRelUpdatable(Relation rel);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
@@ -579,6 +588,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->EndForeignModify = postgresEndForeignModify;
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
+	routine->BeginForeignCopy = postgresBeginForeignCopy;
+	routine->ExecForeignCopy = postgresExecForeignCopy;
+	routine->EndForeignCopy = postgresEndForeignCopy;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
@@ -2209,6 +2221,135 @@ postgresEndForeignInsert(EState *estate,
 	finish_foreign_modify(fmstate);
 }
 
+static PgFdwModifyState *copy_fmstate = NULL;
+
+static void
+pgfdw_copy_dest_cb(void *buf, int len)
+{
+	PGconn *conn = copy_fmstate->conn;
+
+	if (PQputCopyData(conn, (char *) buf, len) <= 0)
+		pgfdw_report_error(ERROR, NULL, conn, false, copy_fmstate->query);
+}
+
+/*
+ * postgresBeginForeignCopy
+ *		Begin a COPY operation on a foreign table
+ */
+static void
+postgresBeginForeignCopy(EState *estate,
+						   ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate;
+	StringInfoData sql;
+	RangeTblEntry *rte;
+	Relation rel = resultRelInfo->ri_RelationDesc;
+
+	if (resultRelInfo->ri_RangeTableIndex == 0)
+	{
+		ResultRelInfo *rootResultRelInfo = resultRelInfo->ri_RootResultRelInfo;
+
+		Assert(rootResultRelInfo != NULL);
+		rte = exec_rt_fetch(rootResultRelInfo->ri_RangeTableIndex, estate);
+		rte = copyObject(rte);
+		rte->relid = RelationGetRelid(rel);
+		rte->relkind = RELKIND_FOREIGN_TABLE;
+	}
+	else
+		rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, estate);
+
+	initStringInfo(&sql);
+	deparseCopyFromSql(&sql, rel);
+
+	fmstate = create_foreign_modify(estate,
+									rte,
+									resultRelInfo,
+									CMD_INSERT,
+									NULL,
+									sql.data,
+									NIL,
+									-1,
+									false,
+									NIL);
+
+	fmstate->cstate = BeginCopyTo(NULL, rel, NULL,
+								  InvalidOid, NULL, false, pgfdw_copy_dest_cb,
+								  NIL, NIL);
+	CopyToStart(fmstate->cstate);
+	resultRelInfo->ri_FdwState = fmstate;
+}
+
+/*
+ * postgresExecForeignCopy
+ *		Send a number of tuples to the foreign relation.
+ */
+static void
+postgresExecForeignCopy(ResultRelInfo *resultRelInfo,
+						  TupleTableSlot **slots, int nslots)
+{
+	PgFdwModifyState *fmstate = resultRelInfo->ri_FdwState;
+	PGresult *res;
+	PGconn *conn = fmstate->conn;
+	bool OK = false;
+	int i;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+
+	res = PQexec(conn, fmstate->query);
+	if (PQresultStatus(res) != PGRES_COPY_IN)
+		pgfdw_report_error(ERROR, res, conn, true, fmstate->query);
+	PQclear(res);
+
+	PG_TRY();
+	{
+		copy_fmstate = fmstate;
+		for (i = 0; i < nslots; i++)
+			CopyOneRowTo(fmstate->cstate, slots[i]);
+
+		OK = true;
+	}
+	PG_FINALLY();
+	{
+		/*
+		 * Finish COPY IN protocol. It is needed to do after successful copy or
+		 * after an error.
+		 */
+		if (PQputCopyEnd(conn, OK ? NULL : "canceled by server") <= 0)
+			pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query);
+
+		/* After successfully  sending an EOF signal, check command OK. */
+		res = PQgetResult(conn);
+		if (PQresultStatus(res) != PGRES_COMMAND_OK)
+			pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
+
+		PQclear(res);
+		/* Do this to ensure we have not gotten extra results */
+		if (PQgetResult(conn) != NULL)
+			ereport(ERROR,
+					(errmsg("unexpected extra results during COPY of table: %s",
+							PQerrorMessage(conn))));
+	}
+	PG_END_TRY();
+}
+
+/*
+ * postgresEndForeignCopy
+ *		Finish a COPY operation on a foreign table
+ */
+static void
+postgresEndForeignCopy(EState *estate, ResultRelInfo *resultRelInfo)
+{
+	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+
+	/* Check correct use of CopyIn FDW API. */
+	Assert(fmstate->cstate != NULL);
+	CopyToFinish(fmstate->cstate);
+	pfree(fmstate->cstate);
+	fmstate->cstate = NULL;
+	finish_foreign_modify(fmstate);
+}
+
 /*
  * postgresIsForeignRelUpdatable
  *		Determine whether a foreign table supports INSERT, UPDATE and/or
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 5d44b75314..10392f6ec2 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -179,6 +179,7 @@ extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 extern void rebuildInsertSql(StringInfo buf, char *orig_query,
 							 int values_end_len, int num_cols,
 							 int num_rows);
+extern void deparseCopyFromSql(StringInfo buf, Relation rel);
 extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 							 Index rtindex, Relation rel,
 							 List *targetAttrs,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 7487096eac..32062b4a55 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2237,6 +2237,23 @@ alter table loc2 drop constraint loc2_f1positive;
 
 delete from rem2;
 
+create table foo (a int) partition by list (a);
+create table foo1 (like foo);
+create foreign table ffoo1 partition of foo for values in (1)
+	server loopback options (table_name 'foo1');
+create table foo2 (like foo);
+create foreign table ffoo2 partition of foo for values in (2)
+	server loopback options (table_name 'foo2');
+create function print_new_row() returns trigger language plpgsql as $$
+	begin raise notice '%', new; return new; end; $$;
+create trigger ffoo1_br_trig before insert on ffoo1
+	for each row execute function print_new_row();
+
+copy foo from stdin;
+1
+2
+\.
+
 -- Test local triggers
 create trigger trig_stmt_before before insert on rem2
 	for each statement execute procedure trigger_func();
@@ -2337,6 +2354,34 @@ drop trigger loc2_trig_row_before_insert on loc2;
 
 delete from rem2;
 
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+1	foo
+2	bar
+\.
+
+alter table loc2 add column f1 int;
+alter table loc2 add column f2 int;
+select * from rem2;
+
+-- dropped columns locally and on the foreign server
+alter table rem2 drop column f1;
+alter table rem2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
+alter table loc2 drop column f1;
+alter table loc2 drop column f2;
+copy rem2 from stdin;
+
+
+\.
+select * from rem2;
+
 -- test COPY FROM with foreign table created in the same transaction
 create table loc3 (f1 int, f2 text);
 begin;
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 98882ddab8..fad2ff6161 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -822,8 +822,9 @@ BeginForeignInsert(ModifyTableState *mtstate,
 
      Begin executing an insert operation on a foreign table.  This routine is
      called right before the first tuple is inserted into the foreign table
-     in both cases when it is the partition chosen for tuple routing and the
-     target specified in a <command>COPY FROM</command> command.  It should
+     target specified in a <command>COPY FROM</command> command, or when
+     the foreign table is the partition chosen for tuple routing of a
+     partitioned table.  It should
      perform any initialization needed prior to the actual insertion.
      Subsequently, <function>ExecForeignInsert</function> or
      <function>ExecForeignBatchInsert</function> will be called for
@@ -1137,6 +1138,72 @@ ExecForeignTruncate(List *rels, List *rels_extra,
 
     <para>
 <programlisting>
+void
+BeginForeignCopy(EState *estate,
+                   ResultRelInfo *rinfo);
+</programlisting>
+
+     Begin executing a copy operation on a foreign table. This routine is
+     called right before the first call of <function>ExecForeignCopy</function>
+     routine for the foreign table. It should perform any initialization needed
+     prior to the actual COPY FROM operation.
+     Subsequently, <function>ExecForeignCopy</function> will be called for
+     a batch of tuples to be copied into the foreign table.
+    </para>
+
+    <para>
+     <literal>estate</literal> is global execution state for the query.
+     <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+     <structname>ResultRelInfo</structname> is available for the FDW to store any
+     private state it needs for this operation.)
+    </para>
+
+    <para>
+     If the <function>BeginForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the initialization.
+    </para>
+
+    <para>
+<programlisting>
+void
+ExecForeignCopy(ResultRelInfo *rinfo,
+                  TupleTableSlot **slots,
+                  int nslots);
+</programlisting>
+
+     Copy a batch of tuples into the foreign table.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+     the target foreign table.
+     <literal>slots</literal> contains the tuples to be inserted; it will match the
+     row-type definition of the foreign table.
+     <literal>nslots</literal> is the number of tuples in the <literal>slots</literal>
+    </para>
+
+    <para>
+     If the <function>ExecForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, the <function>ExecForeignInsert</function> routine will be used to run COPY on the foreign table.
+    </para>
+
+    <para>
+<programlisting>
+void
+EndForeignCopy(EState *estate,
+                 ResultRelInfo *rinfo);
+</programlisting>
+
+     End the copy operation and release resources.  It is normally not important
+     to release palloc'd memory, but for example open files and connections
+     to remote servers should be cleaned up.
+    </para>
+
+    <para>
+     If the <function>EndForeignCopy</function> pointer is set to
+     <literal>NULL</literal>, no action is taken for the termination.
+    </para>
+
+    <para>
+<programlisting>
 RowMarkType
 GetForeignRowMarkType(RangeTblEntry *rte,
                       LockClauseStrength strength);
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 8265b981eb..f646770767 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -304,7 +304,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 		CopyToState cstate;
 
 		cstate = BeginCopyTo(pstate, rel, query, relid,
-							 stmt->filename, stmt->is_program,
+							 stmt->filename, stmt->is_program, NULL,
 							 stmt->attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 20e7d57d41..b486ffd641 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -317,54 +317,64 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo,
 	cstate->line_buf_valid = false;
 	save_cur_lineno = cstate->cur_lineno;
 
-	/*
-	 * table_multi_insert may leak memory, so switch to short-lived memory
-	 * context before calling it.
-	 */
-	oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
-	table_multi_insert(resultRelInfo->ri_RelationDesc,
-					   slots,
-					   nused,
-					   mycid,
-					   ti_options,
-					   buffer->bistate);
-	MemoryContextSwitchTo(oldcontext);
-
-	for (i = 0; i < nused; i++)
+	if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+	{
+		/* Flush into foreign table or partition */
+		resultRelInfo->ri_FdwRoutine->ExecForeignCopy(resultRelInfo,
+														slots,
+														nused);
+	}
+	else
 	{
 		/*
-		 * If there are any indexes, update them for all the inserted tuples,
-		 * and run AFTER ROW INSERT triggers.
+		 * table_multi_insert may leak memory, so switch to short-lived memory
+		 * context before calling it.
 		 */
-		if (resultRelInfo->ri_NumIndices > 0)
-		{
-			List	   *recheckIndexes;
-
-			cstate->cur_lineno = buffer->linenos[i];
-			recheckIndexes =
-				ExecInsertIndexTuples(resultRelInfo,
-									  buffer->slots[i], estate, false, false,
-									  NULL, NIL);
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], recheckIndexes,
-								 cstate->transition_capture);
-			list_free(recheckIndexes);
-		}
+		oldcontext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));
+		table_multi_insert(resultRelInfo->ri_RelationDesc,
+						   slots,
+						   nused,
+						   mycid,
+						   ti_options,
+						   buffer->bistate);
+		MemoryContextSwitchTo(oldcontext);
 
-		/*
-		 * There's no indexes, but see if we need to run AFTER ROW INSERT
-		 * triggers anyway.
-		 */
-		else if (resultRelInfo->ri_TrigDesc != NULL &&
-				 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
-				  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+		for (i = 0; i < nused; i++)
 		{
-			cstate->cur_lineno = buffer->linenos[i];
-			ExecARInsertTriggers(estate, resultRelInfo,
-								 slots[i], NIL, cstate->transition_capture);
-		}
+			/*
+			 * If there are any indexes, update them for all the inserted tuples,
+			 * and run AFTER ROW INSERT triggers.
+			 */
+			if (resultRelInfo->ri_NumIndices > 0)
+			{
+				List	   *recheckIndexes;
+
+				cstate->cur_lineno = buffer->linenos[i];
+				recheckIndexes =
+					ExecInsertIndexTuples(resultRelInfo,
+										  buffer->slots[i], estate, false, false,
+										  NULL, NIL);
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], recheckIndexes,
+									 cstate->transition_capture);
+				list_free(recheckIndexes);
+			}
+
+			/*
+			 * There's no indexes, but see if we need to run AFTER ROW INSERT
+			 * triggers anyway.
+			 */
+			else if (resultRelInfo->ri_TrigDesc != NULL &&
+					 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+					  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
+			{
+				cstate->cur_lineno = buffer->linenos[i];
+				ExecARInsertTriggers(estate, resultRelInfo,
+									 slots[i], NIL, cstate->transition_capture);
+			}
 
-		ExecClearTuple(slots[i]);
+			ExecClearTuple(slots[i]);
+		}
 	}
 
 	/* Mark that all slots are free */
@@ -538,13 +548,11 @@ CopyFrom(CopyFromState cstate)
 	CommandId	mycid = GetCurrentCommandId(true);
 	int			ti_options = 0; /* start with default options for insert */
 	BulkInsertState bistate = NULL;
-	CopyInsertMethod insertMethod;
 	CopyMultiInsertInfo multiInsertInfo = {0};	/* pacify compiler */
 	int64		processed = 0;
 	int64		excluded = 0;
 	bool		has_before_insert_row_trig;
 	bool		has_instead_insert_row_trig;
-	bool		leafpart_use_multi_insert = false;
 
 	Assert(cstate->rel);
 	Assert(list_length(cstate->range_table) == 1);
@@ -654,6 +662,33 @@ CopyFrom(CopyFromState cstate)
 	resultRelInfo = target_resultRelInfo = makeNode(ResultRelInfo);
 	ExecInitResultRelation(estate, resultRelInfo, 1);
 
+	Assert(!target_resultRelInfo->ri_usesMultiInsert);
+
+	/*
+	 * It's generally more efficient to prepare a bunch of tuples for
+	 * insertion, and insert them in bulk, for example, with one
+	 * table_multi_insert() call than call table_tuple_insert() separately for
+	 * every tuple. However, there are a number of reasons why we might not be
+	 * able to do this.  For example, if there any volatile expressions in the
+	 * table's default values or in the statement's WHERE clause, which may
+	 * query the table we are inserting into, buffering tuples might produce
+	 * wrong results.  Also, the relation we are trying to insert into itself
+	 * may not be amenable to buffered inserts.
+	 *
+	 * Note: For partitions, this flag is set considering the target table's
+	 * flag that is being set here and partition's own properties which are
+	 * checked by calling ExecMultiInsertAllowed().  It does not matter
+	 * whether partitions have any volatile default expressions as we use the
+	 * defaults from the target of the COPY command.
+	 * Also, the COPY command requires a non-zero input list of attributes.
+	 * Therefore, the length of the attribute list is checked here.
+	 */
+	if (!cstate->volatile_defexprs &&
+		list_length(cstate->attnumlist) > 0 &&
+		!contain_volatile_functions(cstate->whereClause))
+		target_resultRelInfo->ri_usesMultiInsert =
+					ExecMultiInsertAllowed(target_resultRelInfo);
+
 	/* Verify the named relation is a valid target for INSERT */
 	CheckValidResultRel(resultRelInfo, CMD_INSERT);
 
@@ -671,10 +706,22 @@ CopyFrom(CopyFromState cstate)
 	mtstate->resultRelInfo = resultRelInfo;
 	mtstate->rootResultRelInfo = resultRelInfo;
 
-	if (resultRelInfo->ri_FdwRoutine != NULL &&
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
-														 resultRelInfo);
+	/*
+	 * Init copying process into foreign table. Initialization of copying into
+	 * foreign partitions will be done later.
+	 */
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			if (target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL)
+				target_resultRelInfo->ri_FdwRoutine->BeginForeignCopy(estate,
+																	  resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+																	resultRelInfo);
+	}
 
 	/* Prepare to catch AFTER triggers. */
 	AfterTriggerBeginQuery();
@@ -703,83 +750,9 @@ CopyFrom(CopyFromState cstate)
 		cstate->qualexpr = ExecInitQual(castNode(List, cstate->whereClause),
 										&mtstate->ps);
 
-	/*
-	 * It's generally more efficient to prepare a bunch of tuples for
-	 * insertion, and insert them in one table_multi_insert() call, than call
-	 * table_tuple_insert() separately for every tuple. However, there are a
-	 * number of reasons why we might not be able to do this.  These are
-	 * explained below.
-	 */
-	if (resultRelInfo->ri_TrigDesc != NULL &&
-		(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
-		 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
-	{
-		/*
-		 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
-		 * triggers on the table. Such triggers might query the table we're
-		 * inserting into and act differently if the tuples that have already
-		 * been processed and prepared for insertion are not there.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (proute != NULL && resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_new_table)
-	{
-		/*
-		 * For partitioned tables we can't support multi-inserts when there
-		 * are any statement level insert triggers. It might be possible to
-		 * allow partitioned tables with such triggers in the future, but for
-		 * now, CopyMultiInsertInfoFlush expects that any before row insert
-		 * and statement level insert triggers are on the same relation.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (resultRelInfo->ri_FdwRoutine != NULL ||
-			 cstate->volatile_defexprs)
-	{
-		/*
-		 * Can't support multi-inserts to foreign tables or if there are any
-		 * volatile default expressions in the table.  Similarly to the
-		 * trigger case above, such expressions may query the table we're
-		 * inserting into.
-		 *
-		 * Note: It does not matter if any partitions have any volatile
-		 * default expressions as we use the defaults from the target of the
-		 * COPY command.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else if (contain_volatile_functions(cstate->whereClause))
-	{
-		/*
-		 * Can't support multi-inserts if there are any volatile function
-		 * expressions in WHERE clause.  Similarly to the trigger case above,
-		 * such expressions may query the table we're inserting into.
-		 */
-		insertMethod = CIM_SINGLE;
-	}
-	else
-	{
-		/*
-		 * For partitioned tables, we may still be able to perform bulk
-		 * inserts.  However, the possibility of this depends on which types
-		 * of triggers exist on the partition.  We must disable bulk inserts
-		 * if the partition is a foreign table or it has any before row insert
-		 * or insert instead triggers (same as we checked above for the parent
-		 * table).  Since the partition's resultRelInfos are initialized only
-		 * when we actually need to insert the first tuple into them, we must
-		 * have the intermediate insert method of CIM_MULTI_CONDITIONAL to
-		 * flag that we must later determine if we can use bulk-inserts for
-		 * the partition being inserted into.
-		 */
-		if (proute)
-			insertMethod = CIM_MULTI_CONDITIONAL;
-		else
-			insertMethod = CIM_MULTI;
-
+	if (resultRelInfo->ri_usesMultiInsert)
 		CopyMultiInsertInfoInit(&multiInsertInfo, resultRelInfo, cstate,
 								estate, mycid, ti_options);
-	}
 
 	/*
 	 * If not using batch mode (which allocates slots as needed) set up a
@@ -787,7 +760,7 @@ CopyFrom(CopyFromState cstate)
 	 * one, even if we might batch insert, to read the tuple in the root
 	 * partition's form.
 	 */
-	if (insertMethod == CIM_SINGLE || insertMethod == CIM_MULTI_CONDITIONAL)
+	if (!resultRelInfo->ri_usesMultiInsert || proute)
 	{
 		singleslot = table_slot_create(resultRelInfo->ri_RelationDesc,
 									   &estate->es_tupleTable);
@@ -830,7 +803,7 @@ CopyFrom(CopyFromState cstate)
 		ResetPerTupleExprContext(estate);
 
 		/* select slot to (initially) load row into */
-		if (insertMethod == CIM_SINGLE || proute)
+		if (!target_resultRelInfo->ri_usesMultiInsert || proute)
 		{
 			myslot = singleslot;
 			Assert(myslot != NULL);
@@ -838,7 +811,6 @@ CopyFrom(CopyFromState cstate)
 		else
 		{
 			Assert(resultRelInfo == target_resultRelInfo);
-			Assert(insertMethod == CIM_MULTI);
 
 			myslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 													 resultRelInfo);
@@ -905,24 +877,14 @@ CopyFrom(CopyFromState cstate)
 				has_instead_insert_row_trig = (resultRelInfo->ri_TrigDesc &&
 											   resultRelInfo->ri_TrigDesc->trig_insert_instead_row);
 
-				/*
-				 * Disable multi-inserts when the partition has BEFORE/INSTEAD
-				 * OF triggers, or if the partition is a foreign partition.
-				 */
-				leafpart_use_multi_insert = insertMethod == CIM_MULTI_CONDITIONAL &&
-					!has_before_insert_row_trig &&
-					!has_instead_insert_row_trig &&
-					resultRelInfo->ri_FdwRoutine == NULL;
-
 				/* Set the multi-insert buffer to use for this partition. */
-				if (leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					if (resultRelInfo->ri_CopyMultiInsertBuffer == NULL)
 						CopyMultiInsertInfoSetupBuffer(&multiInsertInfo,
 													   resultRelInfo);
 				}
-				else if (insertMethod == CIM_MULTI_CONDITIONAL &&
-						 !CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+				else if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
 				{
 					/*
 					 * Flush pending inserts if this partition can't use
@@ -952,7 +914,7 @@ CopyFrom(CopyFromState cstate)
 			 * rowtype.
 			 */
 			map = resultRelInfo->ri_RootToPartitionMap;
-			if (insertMethod == CIM_SINGLE || !leafpart_use_multi_insert)
+			if (!resultRelInfo->ri_usesMultiInsert)
 			{
 				/* non batch insert */
 				if (map != NULL)
@@ -971,9 +933,6 @@ CopyFrom(CopyFromState cstate)
 				 */
 				TupleTableSlot *batchslot;
 
-				/* no other path available for partitioned table */
-				Assert(insertMethod == CIM_MULTI_CONDITIONAL);
-
 				batchslot = CopyMultiInsertInfoNextFreeSlot(&multiInsertInfo,
 															resultRelInfo);
 
@@ -1045,7 +1004,7 @@ CopyFrom(CopyFromState cstate)
 					ExecPartitionCheck(resultRelInfo, myslot, estate, true);
 
 				/* Store the slot in the multi-insert buffer, when enabled. */
-				if (insertMethod == CIM_MULTI || leafpart_use_multi_insert)
+				if (resultRelInfo->ri_usesMultiInsert)
 				{
 					/*
 					 * The slot previously might point into the per-tuple
@@ -1124,11 +1083,8 @@ CopyFrom(CopyFromState cstate)
 	}
 
 	/* Flush any remaining buffered tuples */
-	if (insertMethod != CIM_SINGLE)
-	{
-		if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
-			CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
-	}
+	if (!CopyMultiInsertInfoIsEmpty(&multiInsertInfo))
+		CopyMultiInsertInfoFlush(&multiInsertInfo, NULL);
 
 	/* Done, clean up */
 	error_context_stack = errcallback.previous;
@@ -1147,14 +1103,21 @@ CopyFrom(CopyFromState cstate)
 	ExecResetTupleTable(estate->es_tupleTable, false);
 
 	/* Allow the FDW to shut down */
-	if (target_resultRelInfo->ri_FdwRoutine != NULL &&
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-		target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
-															  target_resultRelInfo);
+	if (target_resultRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (target_resultRelInfo->ri_usesMultiInsert)
+		{
+			if (target_resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+				target_resultRelInfo->ri_FdwRoutine->EndForeignCopy(estate,
+																	target_resultRelInfo);
+		}
+		else if (target_resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+			target_resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+														target_resultRelInfo);
+	}
 
 	/* Tear down the multi-insert buffer data */
-	if (insertMethod != CIM_SINGLE)
-		CopyMultiInsertInfoCleanup(&multiInsertInfo);
+	CopyMultiInsertInfoCleanup(&multiInsertInfo);
 
 	/* Close all the partitioned tables, leaf partitions, and their indices */
 	if (proute)
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 7257a54e93..378233655d 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -51,6 +51,7 @@ typedef enum CopyDest
 {
 	COPY_FILE,					/* to file (or a piped program) */
 	COPY_FRONTEND,				/* to frontend */
+	COPY_CALLBACK				/* to callback function */
 } CopyDest;
 
 /*
@@ -86,6 +87,7 @@ typedef struct CopyToStateData
 	char	   *filename;		/* filename, or NULL for STDOUT */
 	bool		is_program;		/* is 'filename' a program to popen? */
 
+	copy_data_dest_cb data_dest_cb;	/* function for writing data */
 	CopyFormatOptions opts;
 	Node	   *whereClause;	/* WHERE condition (or NULL) */
 
@@ -115,7 +117,6 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0";
 /* non-export function prototypes */
 static void EndCopy(CopyToState cstate);
 static void ClosePipeToProgram(CopyToState cstate);
-static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 static void CopyAttributeOutText(CopyToState cstate, char *string);
 static void CopyAttributeOutCSV(CopyToState cstate, char *string,
 								bool use_quote, bool single_attr);
@@ -248,6 +249,15 @@ CopySendEndOfRow(CopyToState cstate)
 			/* Dump the accumulated row as one CopyData message */
 			(void) pq_putmessage('d', fe_msgbuf->data, fe_msgbuf->len);
 			break;
+		case COPY_CALLBACK:
+			Assert(!cstate->opts.binary);
+#ifndef WIN32
+			CopySendChar(cstate, '\n');
+#else
+			CopySendString(cstate, "\r\n");
+#endif
+			cstate->data_dest_cb(fe_msgbuf->data, fe_msgbuf->len);
+			break;
 	}
 
 	/* Update the progress */
@@ -345,11 +355,12 @@ BeginCopyTo(ParseState *pstate,
 			Oid queryRelId,
 			const char *filename,
 			bool is_program,
+			copy_data_dest_cb data_dest_cb,
 			List *attnamelist,
 			List *options)
 {
 	CopyToState	cstate;
-	bool		pipe = (filename == NULL);
+	bool		pipe = (filename == NULL) && (data_dest_cb == NULL);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	MemoryContext oldcontext;
@@ -362,7 +373,13 @@ BeginCopyTo(ParseState *pstate,
 		0
 	};
 
-	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION)
+	/*
+	 * Check whether we support copying data out of the specified relation,
+	 * unless the caller also passed a non-NULL data_dest_cb, in which case,
+	 * the callback will take care of it
+	 */
+	if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION &&
+		data_dest_cb == NULL)
 	{
 		if (rel->rd_rel->relkind == RELKIND_VIEW)
 			ereport(ERROR,
@@ -673,6 +690,11 @@ BeginCopyTo(ParseState *pstate,
 		if (whereToSendOutput != DestRemote)
 			cstate->copy_file = stdout;
 	}
+	else if (data_dest_cb)
+	{
+		cstate->copy_dest = COPY_CALLBACK;
+		cstate->data_dest_cb = data_dest_cb;
+	}
 	else
 	{
 		cstate->filename = pstrdup(filename);
@@ -773,20 +795,17 @@ EndCopyTo(CopyToState cstate)
 }
 
 /*
- * Copy from relation or query TO file.
+ * Start COPY TO operation.
+ * Separate from the main routine to prevent duplicate operations in
+ * manual mode, where tuples are copied to the destination one by one, by calling
+ * the CopyOneRowTo() routine.
  */
-uint64
-DoCopyTo(CopyToState cstate)
+void
+CopyToStart(CopyToState cstate)
 {
-	bool		pipe = (cstate->filename == NULL);
-	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
 	TupleDesc	tupDesc;
 	int			num_phys_attrs;
 	ListCell   *cur;
-	uint64		processed;
-
-	if (fe_copy)
-		SendCopyBegin(cstate);
 
 	if (cstate->rel)
 		tupDesc = RelationGetDescr(cstate->rel);
@@ -876,6 +895,39 @@ DoCopyTo(CopyToState cstate)
 			CopySendEndOfRow(cstate);
 		}
 	}
+}
+
+/*
+ * Finish COPY TO operation.
+ */
+void
+CopyToFinish(CopyToState cstate)
+{
+	if (cstate->opts.binary)
+	{
+		/* Generate trailer for a binary copy */
+		CopySendInt16(cstate, -1);
+		/* Need to flush out the trailer */
+		CopySendEndOfRow(cstate);
+	}
+
+	MemoryContextDelete(cstate->rowcontext);
+}
+
+/*
+ * Copy from relation or query TO file.
+ */
+uint64
+DoCopyTo(CopyToState cstate)
+{
+	bool		pipe = (cstate->filename == NULL) && (cstate->data_dest_cb == NULL);
+	bool		fe_copy = (pipe && whereToSendOutput == DestRemote);
+	uint64		processed;
+
+	if (fe_copy)
+		SendCopyBegin(cstate);
+
+	CopyToStart(cstate);
 
 	if (cstate->rel)
 	{
@@ -914,15 +966,7 @@ DoCopyTo(CopyToState cstate)
 		processed = ((DR_copy *) cstate->queryDesc->dest)->processed;
 	}
 
-	if (cstate->opts.binary)
-	{
-		/* Generate trailer for a binary copy */
-		CopySendInt16(cstate, -1);
-		/* Need to flush out the trailer */
-		CopySendEndOfRow(cstate);
-	}
-
-	MemoryContextDelete(cstate->rowcontext);
+	CopyToFinish(cstate);
 
 	if (fe_copy)
 		SendCopyEnd(cstate);
@@ -933,7 +977,7 @@ DoCopyTo(CopyToState cstate)
 /*
  * Emit one row during DoCopyTo().
  */
-static void
+void
 CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
 {
 	bool		need_delim = false;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b2e2df8773..f9049cfae4 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1254,9 +1254,53 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_PartitionTupleSlot = NULL;	/* ditto */
 	resultRelInfo->ri_ChildToRootMap = NULL;
 	resultRelInfo->ri_ChildToRootMapValid = false;
+	resultRelInfo->ri_usesMultiInsert = false;
 	resultRelInfo->ri_CopyMultiInsertBuffer = NULL;
 }
 
+/*
+ * ExecMultiInsertAllowed
+ *		Does this relation allow caller to use multi-insert mode when
+ *		inserting rows into it?
+ */
+bool
+ExecMultiInsertAllowed(const ResultRelInfo *rri)
+{
+	/*
+	 * Can't support multi-inserts when there are any BEFORE/INSTEAD OF
+	 * triggers on the table. Such triggers might query the table we're
+	 * inserting into and act differently if the tuples that have already
+	 * been processed and prepared for insertion are not there.
+	 */
+	if (rri->ri_TrigDesc != NULL &&
+		(rri->ri_TrigDesc->trig_insert_before_row ||
+		 rri->ri_TrigDesc->trig_insert_instead_row))
+		return false;
+
+	/*
+	 * For partitioned tables we can't support multi-inserts when there are
+	 * any statement level insert triggers. It might be possible to allow
+	 * partitioned tables with such triggers in the future, but for now,
+	 * CopyMultiInsertInfoFlush expects that any before row insert and
+	 * statement level insert triggers are on the same relation.
+	 */
+	if (rri->ri_RelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+		rri->ri_TrigDesc != NULL &&
+		rri->ri_TrigDesc->trig_insert_new_table)
+		return false;
+
+	if (rri->ri_FdwRoutine != NULL &&
+		rri->ri_FdwRoutine->ExecForeignCopy == NULL)
+		/*
+		 * Foreign tables don't support multi-inserts, unless their FDW
+		 * provides the necessary COPY interface.
+		 */
+		return false;
+
+	/* OK, caller can use multi-insert on this relation. */
+	return true;
+}
+
 /*
  * ExecGetTriggerResultRel
  *		Get a ResultRelInfo for a trigger target relation.
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 99780ebb96..f402e13b9b 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -514,6 +514,14 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 					  rootResultRelInfo,
 					  estate->es_instrument);
 
+	/*
+	 * If a partition's root parent isn't allowed to use it, neither is the
+	 * partition.
+	 */
+	if (rootResultRelInfo->ri_usesMultiInsert)
+		leaf_part_rri->ri_usesMultiInsert =
+			ExecMultiInsertAllowed(leaf_part_rri);
+
 	/*
 	 * Verify result relation is a valid target for an INSERT.  An UPDATE of a
 	 * partition-key becomes a DELETE+INSERT operation, so this check is still
@@ -907,9 +915,16 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 	 * If the partition is a foreign table, let the FDW init itself for
 	 * routing tuples to the partition.
 	 */
-	if (partRelInfo->ri_FdwRoutine != NULL &&
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
-		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	if (partRelInfo->ri_FdwRoutine != NULL)
+	{
+		if (partRelInfo->ri_usesMultiInsert)
+		{
+			if (partRelInfo->ri_FdwRoutine->BeginForeignCopy != NULL)
+				partRelInfo->ri_FdwRoutine->BeginForeignCopy(estate, partRelInfo);
+		}
+		else if (partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+			partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+	}
 
 	/*
 	 * Determine if the FDW supports batch insert and determine the batch
@@ -1146,10 +1161,18 @@ ExecCleanupTupleRouting(ModifyTableState *mtstate,
 		ResultRelInfo *resultRelInfo = proute->partitions[i];
 
 		/* Allow any FDWs to shut down */
-		if (resultRelInfo->ri_FdwRoutine != NULL &&
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
-			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
-														   resultRelInfo);
+		if (resultRelInfo->ri_FdwRoutine != NULL)
+		{
+			if (resultRelInfo->ri_usesMultiInsert)
+			{
+				if (resultRelInfo->ri_FdwRoutine->EndForeignCopy != NULL)
+					resultRelInfo->ri_FdwRoutine->EndForeignCopy(mtstate->ps.state,
+																 resultRelInfo);
+			}
+			else if (resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+				resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+															   resultRelInfo);
+		}
 
 		/*
 		 * Close it if it's not one of the result relations borrowed from the
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8c4748e33d..3d9d187765 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -55,6 +55,7 @@ typedef struct CopyFromStateData *CopyFromState;
 typedef struct CopyToStateData *CopyToState;
 
 typedef int (*copy_data_source_cb) (void *outbuf, int minread, int maxread);
+typedef void (*copy_data_dest_cb) (void *outbuf, int len);
 
 extern void DoCopy(ParseState *state, const CopyStmt *stmt,
 				   int stmt_location, int stmt_len,
@@ -80,10 +81,14 @@ extern DestReceiver *CreateCopyDestReceiver(void);
  */
 extern CopyToState BeginCopyTo(ParseState *pstate, Relation rel, RawStmt *query,
 							   Oid queryRelId, const char *filename, bool is_program,
+							   copy_data_dest_cb data_dest_cb,
 							   List *attnamelist, List *options);
 extern void EndCopyTo(CopyToState cstate);
 extern uint64 DoCopyTo(CopyToState cstate);
 extern List *CopyGetAttnums(TupleDesc tupDesc, Relation rel,
 							List *attnamelist);
+extern void CopyToStart(CopyToState cstate);
+extern void CopyToFinish(CopyToState cstate);
+extern void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot);
 
 #endif							/* COPY_H */
diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h
index 858af7a717..8f61ff3d4d 100644
--- a/src/include/commands/copyfrom_internal.h
+++ b/src/include/commands/copyfrom_internal.h
@@ -39,16 +39,6 @@ typedef enum EolType
 	EOL_CRNL
 } EolType;
 
-/*
- * Represents the heap insert method to be used during COPY FROM.
- */
-typedef enum CopyInsertMethod
-{
-	CIM_SINGLE,					/* use table_tuple_insert or fdw routine */
-	CIM_MULTI,					/* always use table_multi_insert */
-	CIM_MULTI_CONDITIONAL		/* use table_multi_insert only if valid */
-} CopyInsertMethod;
-
 /*
  * This struct contains all the state variables used throughout a COPY FROM
  * operation.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 6eae134c08..beb8e8fcd0 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,6 +203,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
 							  Index resultRelationIndex,
 							  ResultRelInfo *partition_root_rri,
 							  int instrument_options);
+extern bool ExecMultiInsertAllowed(const ResultRelInfo *rri);
 extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
 extern void ExecConstraints(ResultRelInfo *resultRelInfo,
 							TupleTableSlot *slot, EState *estate);
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 4ebbca6de9..74fe6bdf5c 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -127,6 +127,16 @@ typedef TupleTableSlot *(*IterateDirectModify_function) (ForeignScanState *node)
 
 typedef void (*EndDirectModify_function) (ForeignScanState *node);
 
+typedef void (*BeginForeignCopy_function) (EState *estate,
+										   ResultRelInfo *rinfo);
+
+typedef void (*ExecForeignCopy_function) (ResultRelInfo *rinfo,
+										  TupleTableSlot **slots,
+										  int nslots);
+
+typedef void (*EndForeignCopy_function) (EState *estate,
+										 ResultRelInfo *rinfo);
+
 typedef RowMarkType (*GetForeignRowMarkType_function) (RangeTblEntry *rte,
 													   LockClauseStrength strength);
 
@@ -244,6 +254,11 @@ typedef struct FdwRoutine
 	IterateDirectModify_function IterateDirectModify;
 	EndDirectModify_function EndDirectModify;
 
+	/* Support functions for COPY into foreign tables */
+	BeginForeignCopy_function BeginForeignCopy;
+	ExecForeignCopy_function ExecForeignCopy;
+	EndForeignCopy_function EndForeignCopy;
+
 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
 	GetForeignRowMarkType_function GetForeignRowMarkType;
 	RefetchForeignRow_function RefetchForeignRow;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index e7ae21c023..8f13c92726 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -521,7 +521,13 @@ typedef struct ResultRelInfo
 	TupleConversionMap *ri_ChildToRootMap;
 	bool		ri_ChildToRootMapValid;
 
-	/* for use by copyfrom.c when performing multi-inserts */
+	/*
+	 * The following fields are currently only relevant to copyfrom.c.
+	 * True if okay to use multi-insert on this relation
+	 */
+	bool ri_usesMultiInsert;
+
+	/* Buffer allocated to this relation when using multi-insert mode */
 	struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
 } ResultRelInfo;
 
-- 
2.17.0


--l0l+eSofNeLXHSnY--





^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* Re: Transparent column encryption
@ 2023-02-22 10:25  Peter Eisentraut <[email protected]>
  0 siblings, 1 reply; 62+ messages in thread

From: Peter Eisentraut @ 2023-02-22 10:25 UTC (permalink / raw)
  To: pgsql-hackers

New patch.

Per some feedback, I have renamed this feature.  People didn't like the 
"transparent", for various reasons.  The new name I came up with is 
"automatic client-side column-level encryption".  This also matches the 
terminology used in other products better.  (Maybe the acronym ACSCLE -- 
pronounced "a chuckle" -- will catch on.)  I'm also using various 
subsets of that name when the context is clear.

Other changes since v15:

- CEKs and CMKs now have USAGE privileges.  (There are some TODO markers 
where I got too bored with boilerplate.  I will fill those in, but the 
idea should be clear.)

- Renamed attrealtypid to attusertypid.  (It wasn't really "real".)
- Added corresponding attusertypmod.
- Removed attencalg, it's now stored in atttypmod.
(The last three together make the whole attribute storage work more 
sensibly and smoothly.)

- Various documentation changes (review by Mark Dilger)
- Added more explicit documentation that this feature is not to protect 
against an "evil DBA".

From db66586f147abebba9d22ea416ae79032ebad5bb Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Wed, 22 Feb 2023 11:11:15 +0100
Subject: [PATCH v16] Automatic client-side column-level encryption

This feature enables the automatic encryption and decryption of
particular columns in the client.  The data for those columns then
only ever appears in ciphertext on the server, so it is protected from
DBAs, sysadmins, cloud operators, etc. as well as accidental leakage
to server logs, file-system backups, etc.  The canonical use case for
this feature is storing credit card numbers encrypted, in accordance
with PCI DSS, as well as similar situations involving social security
numbers etc.  One can't do any computations with encrypted values on
the server, but for these use cases, that is not necessary.  This
feature does support deterministic encryption as an alternative to the
default randomized encryption, so in that mode one can do equality
lookups, at the cost of some security.

This functionality also exists in other database products, and the
overall concepts were mostly adopted from there.

(Note: This feature has nothing to do with any on-disk encryption
feature.  Both can exist independently.)

You declare a column as encrypted in a CREATE TABLE statement.  The
column value is encrypted by a symmetric key called the column
encryption key (CEK).  The CEK is a catalog object.  The CEK key
material is in turn encrypted by an asymmetric key called the column
master key (CMK).  The CMK is not stored in the database but somewhere
where the client can get to it, for example in a file or in a key
management system.  When a server sends rows containing encrypted
column values to the client, it first sends the required CMK and CEK
information (new protocol messages), which the client needs to record.
Then, the client can use this information to automatically decrypt the
incoming row data and forward it in plaintext to the application.

For the CMKs, libpq has a new connection parameter "cmklookup" that
specifies via a mini-language where to get the keys.  Right now, you
can use "file" to read it from a file, or "run" to run some program,
which could get it from a KMS.

The general idea would be for an application to have one CMK per area
of secret stuff, for example, for credit card data.  The CMK can be
rotated: each CEK can be represented multiple times in the database,
encrypted by a different CMK.  (The CEK can't be rotated easily, since
that would require reading out all the data from a table/column and
reencrypting it.  We could/should add some custom tooling for that,
but it wouldn't be a routine operation.)

Several encryption algorithms are provided.  The CMK process uses
RSAES_OAEP_SHA_1 or _256.  The CEK process uses
AEAD_AES_*_CBC_HMAC_SHA_* with several strengths.

In the server, the encrypted datums are stored in types called
pg_encrypted_rnd and pg_encrypted_det (for randomized and
deterministic encryption).  These are essentially cousins of bytea.
For the rest of the database system below the protocol handling, there
is nothing special about those.  For example, pg_encrypted_rnd has no
operators at all, pg_encrypted_det has only an equality operator.
pg_attribute has a new column attrealtypid that stores the original
type of the data in the column.  This is only used for providing it to
clients, so that higher-level clients can convert the decrypted value
to their appropriate data types in their environments.

The protocol extensions are guarded by a new protocol extension option
"_pq_.column_encryption".  If this is not set, nothing changes, the
protocol stays the same, and no encryption or decryption happens.

To get automatically encrypted data into the database (as opposed to
reading it out), it is required to use protocol-level prepared
statements (i.e., extended query).  The client must first prepare a
statement, then describe the statement to get parameter metadata,
which indicates which parameters are to be encrypted and how.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this if the
\bind command is used.

Another challenge is that the parse analysis must check which
underlying column a parameter corresponds to.  This is similar to
resorigtbl and resorigcol in the opposite direction.

Discussion: https://www.postgresql.org/message-id/flat/89157929-c2b6-817b-6025-8e4b2d89d88f%40enterprisedb.com
---
 doc/src/sgml/acronyms.sgml                    |  18 +
 doc/src/sgml/catalogs.sgml                    | 317 +++++++
 doc/src/sgml/charset.sgml                     |  10 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 444 +++++++++
 doc/src/sgml/func.sgml                        |  26 +
 doc/src/sgml/glossary.sgml                    |  26 +
 doc/src/sgml/libpq.sgml                       | 322 +++++++
 doc/src/sgml/protocol.sgml                    | 441 +++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |  10 +
 .../ref/create_column_encryption_key.sgml     | 170 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  55 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/grant.sgml                   |  12 +-
 doc/src/sgml/ref/pg_dump.sgml                 |  42 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   7 +
 src/backend/access/common/printtup.c          | 222 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  60 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 288 ++++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  17 +
 src/backend/commands/colenccmds.c             | 439 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 204 ++++-
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 187 +++-
 src/backend/parser/parse_param.c              | 157 ++++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  53 ++
 src/backend/utils/adt/acl.c                   |  18 +
 src/backend/utils/adt/varlena.c               | 106 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  42 +
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/dumputils.c                   |   4 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 377 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  35 +
 src/bin/pg_dump/pg_dump_sort.c                |  14 +
 src/bin/pg_dump/pg_dumpall.c                  |   5 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  72 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 191 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  67 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |  11 +
 src/include/catalog/pg_colenckey.h            |  46 +
 src/include/catalog/pg_colenckeydata.h        |  46 +
 src/include/catalog/pg_colmasterkey.h         |  47 +
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  10 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               |  41 +
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  43 +-
 src/include/parser/kwlist.h                   |   2 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   6 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 842 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 665 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 139 ++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  24 +
 .../t/001_column_encryption.pl                | 255 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 353 ++++++++
 src/test/regress/expected/object_address.out  |  37 +-
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 265 ++++++
 src/test/regress/sql/object_address.sql       |  13 +-
 src/test/regress/sql/type_sanity.sql          |   2 +
 142 files changed, 9686 insertions(+), 85 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/alter_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_master_key.sgml
 create mode 100644 src/backend/commands/colenccmds.c
 create mode 100644 src/common/colenc.c
 create mode 100644 src/include/catalog/pg_colenckey.h
 create mode 100644 src/include/catalog/pg_colenckeydata.h
 create mode 100644 src/include/catalog/pg_colmasterkey.h
 create mode 100644 src/include/commands/colenccmds.h
 create mode 100644 src/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 create mode 100644 src/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/meson.build
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/t/002_cmk_rotation.pl
 create mode 100644 src/test/column_encryption/test_client.c
 create mode 100755 src/test/column_encryption/test_run_decrypt.pl
 create mode 100644 src/test/regress/expected/column_encryption.out
 create mode 100644 src/test/regress/sql/column_encryption.sql

diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 2df6559acc..3a5f2c254c 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -56,6 +56,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CEK</acronym></term>
+    <listitem>
+     <para>
+      Column Encryption Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CIDR</acronym></term>
     <listitem>
@@ -67,6 +76,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CMK</acronym></term>
+    <listitem>
+     <para>
+      Column Master Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CPAN</acronym></term>
     <listitem>
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1e4048054..808f29669d 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -105,6 +105,21 @@ <title>System Catalogs</title>
       <entry>collations (locale information)</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link></entry>
+      <entry>column encryption keys</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link></entry>
+      <entry>column encryption key data</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link></entry>
+      <entry>column master keys</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link></entry>
       <entry>check constraints, unique constraints, primary key constraints, foreign key constraints</entry>
@@ -1360,6 +1375,44 @@ <title><structname>pg_attribute</structname> Columns</title>
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attcek</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, a reference to the column encryption key, else 0.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type of the
+       encrypted data that is reported to the client.  If the column is not
+       encrypted, then 0.  For encrypted columns, the field
+       <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.
+       </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type
+       modifier (analogous to <structfield>atttypmod</structfield>) that is
+       reported to the client.  If the column is not encrypted, then -1.  For
+       encrypted columns, the field <structfield>atttypmod</structfield>)
+       contains the identifier of the encryption algorithm; see <xref
+       linkend="protocol-cek"/> for possible values.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>attinhcount</structfield> <type>int4</type>
@@ -2467,6 +2520,270 @@ <title><structname>pg_collation</structname> Columns</title>
   </para>
  </sect1>
 
+ <sect1 id="catalog-pg-colenckey">
+  <title><structname>pg_colenckey</structname></title>
+
+  <indexterm zone="catalog-pg-colenckey">
+   <primary>pg_colenckey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckey</structname> contains information
+   about the column encryption keys in the database.  The actual key material
+   of the column encryption keys is in the catalog <link
+   linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link>.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column encryption key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colenckeydata">
+  <title><structname>pg_colenckeydata</structname></title>
+
+  <indexterm zone="catalog-pg-colenckeydata">
+   <primary>pg_colenckeydata</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckeydata</structname> contains the key
+   material of column encryption keys.  Each column encryption key object can
+   contain several versions of the key material, each encrypted with a
+   different column master key.  That allows the gradual rotation of the
+   column master keys.  Thus, <literal>(ckdcekid, ckdcmkid)</literal> is a
+   unique key of this table.
+  </para>
+
+  <para>
+   The key material of column encryption keys should never be decrypted inside
+   the database instance.  It is meant to be sent as-is to the client, where
+   it is decrypted using the associated column master key, and then used to
+   encrypt or decrypt column values.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckeydata</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcekid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column encryption key this entry belongs to
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column master key that the key material is encrypted with
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkalg</structfield> <type>int4</type>
+      </para>
+      <para>
+       The encryption algorithm used for encrypting the key material; see
+       <xref linkend="protocol-cmk"/> for possible values.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdencval</structfield> <type>bytea</type>
+      </para>
+      <para>
+       The key material of this column encryption key, encrypted using the
+       referenced column master key
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colmasterkey">
+  <title><structname>pg_colmasterkey</structname></title>
+
+  <indexterm zone="catalog-pg-colmasterkey">
+   <primary>pg_colmasterkey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colmasterkey</structname> contains information
+   about column master keys.  The keys themselves are not stored in the
+   database.  The catalog entry only contains information that is used by
+   clients to locate the keys, for example in a file or in a key management
+   system.
+  </para>
+
+  <table>
+   <title><structname>pg_colmasterkey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column master key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkrealm</structfield> <type>text</type>
+      </para>
+      <para>
+       A <quote>realm</quote> associated with this column master key.  This is
+       a freely chosen string that is used by clients to determine how to look
+       up the key.  A typical configuration would put all CMKs that are looked
+       up in the same way into the same realm.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml
index 3032392b80..f3026fff83 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,16 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When automatic client-side column-level encryption is used, then no
+     encoding conversion is possible.  (The encoding conversion happens on the
+     server, and the server cannot look inside any encrypted column values.)
+     If automatic client-side column-level encryption is enabled for a
+     session, then the server enforces that the client encoding matches the
+     server encoding, and any attempts to change the client encoding will be
+     rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..67fce16872 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <title>Pseudo-Types</title>
 
   </sect1>
 
+  <sect1 id="datatype-encrypted">
+   <title>Types Related to Encryption</title>
+
+   <para>
+    An encrypted column value (see <xref linkend="ddl-column-encryption"/>) is
+    internally stored using the types
+    <type>pg_encrypted_rnd</type> (for randomized encryption) or
+    <type>pg_encrypted_det</type> (for deterministic encryption); see <xref
+    linkend="datatype-encrypted-table"/>.  Most of the database system treats
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support automatic client-side column-level encryption
+    or have disabled it will see the encrypted values in this format.  Clients
+    that support automatic client-side column-level encryption will not see
+    these types in result sets, as the protocol layer will translate them back
+    to the declared underlying type in the table definition.
+   </para>
+
+    <table id="datatype-encrypted-table">
+     <title>Types Related to Encryption</title>
+     <tgroup cols="3">
+      <colspec colname="col1" colwidth="1*"/>
+      <colspec colname="col2" colwidth="3*"/>
+      <colspec colname="col3" colwidth="2*"/>
+      <thead>
+       <row>
+        <entry>Name</entry>
+        <entry>Storage Size</entry>
+        <entry>Description</entry>
+       </row>
+      </thead>
+      <tbody>
+       <row>
+        <entry><type>pg_encrypted_det</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, deterministic encryption</entry>
+       </row>
+       <row>
+        <entry><type>pg_encrypted_rnd</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, randomized encryption</entry>
+       </row>
+      </tbody>
+     </tgroup>
+    </table>
+  </sect1>
  </chapter>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 8dc8d7a0ce..dfe73ca74c 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,440 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   With <firstterm>automatic client-side column-level encryption</firstterm>,
+   columns can be stored encrypted in the database.  The encryption and
+   decryption happens automatically on the client, so that the plaintext value
+   is never seen in the database instance or on the server hosting the
+   database.  The drawback is that most operations, such as function calls or
+   sorting, are not possible on encrypted values.
+  </para>
+
+  <sect2>
+   <title>Using Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   Automatic client-side column-level encryption uses two levels of
+   cryptographic keys.  The actual column value is encrypted using a symmetric
+   algorithm, such as AES, using a <firstterm>column encryption
+   key</firstterm> (<acronym>CEK</acronym>).  The column encryption key is in
+   turn encrypted using an asymmetric algorithm, such as RSA, using a
+   <firstterm>column master key</firstterm> (<acronym>CMK</acronym>).  The
+   encrypted CEK is stored in the database system.  The CMK is not stored in
+   the database system; it is stored on the client or somewhere where the
+   client can access it, such as in a local file or in a key management
+   system.  The database system only records where the CMK is stored and
+   provides this information to the client.  When rows containing encrypted
+   columns are sent to the client, the server first sends any necessary CMK
+   information, followed by any required CEK.  The client then looks up the
+   CMK and uses that to decrypt the CEK.  Then it decrypts incoming row data
+   using the CEK and provides the decrypted row data to the application.
+  </para>
+
+  <para>
+   Here is an example declaring a column as encrypted:
+<programlisting>
+CREATE TABLE customers (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    creditcard_num text <emphasis>ENCRYPTED WITH (column_encryption_key = cek1)</emphasis>
+);
+</programlisting>
+  </para>
+
+  <para>
+   Column encryption supports <firstterm>randomized</firstterm>
+   (also known as <firstterm>probabilistic</firstterm>) and
+   <firstterm>deterministic</firstterm> encryption.  The above example uses
+   randomized encryption, which is the default.  Randomized encryption uses a
+   random initialization vector for each encryption, so that even if the
+   plaintext of two rows is equal, the encrypted values will be different.
+   This prevents someone with direct access to the database server from making
+   computations such as distinct counts on the encrypted values.
+   Deterministic encryption uses a fixed initialization vector.  This reduces
+   security, but it allows equality searches on encrypted values.  The
+   following example declares a column with deterministic encryption:
+<programlisting>
+CREATE TABLE employees (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    ssn text ENCRYPTED WITH (
+        column_encryption_key = cek1, <emphasis>encryption_type = deterministic</emphasis>)
+);
+</programlisting>
+  </para>
+
+  <para>
+   Null values are not encrypted by automatic client-side column-level
+   encryption; null values sent by the client are visible as null values in
+   the database.  If the fact that a value is null needs to be hidden from the
+   server, this information needs to be encoded into a nonnull value in the
+   client somehow.
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Reading and Writing Encrypted Columns</title>
+
+   <para>
+    Reading and writing encrypted columns is meant to be handled automatically
+    by the client library/driver and should be mostly transparent to the
+    application code, if certain prerequisites are fulfilled:
+
+    <itemizedlist>
+     <listitem>
+      <para>
+       The client library needs to support automatic client-side column-level
+       encryption.  Not all client libraries do.  Furthermore, the client
+       library might require that automatic client-side column-level
+       encryption is explicitly enabled at connection time.  See the
+       documentation of the client library for details.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Column master keys and column encryption keys have been set up, and the
+       client library has been configured to be able to look up column master
+       keys from the key store or key management system.
+      </para>
+     </listitem>
+    </itemizedlist>
+   </para>
+
+   <para>
+    Reading from encrypted columns will then work automatically.  For example,
+    using the above example,
+<programlisting>
+SELECT ssn FROM employees WHERE id = 5;
+</programlisting>
+    would return the unencrypted value for the <literal>ssn</literal> column
+    in any rows found.
+   </para>
+
+   <para>
+    Writing to encrypted columns requires that the extended query protocol
+    (protocol-level prepared statements) be used, so that the values to be
+    encrypted are supplied separately from the SQL command.  For example,
+    using, say, psql or libpq, the following would not work:
+<programlisting>
+-- WRONG!
+INSERT INTO ssn (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This would leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of client-side column-level encryption.
+    (And even ignoring that, it could not work because the server does not
+    have access to the keys to perform the encryption.)  Note that using
+    server-side prepared statements using the SQL commands
+    <command>PREPARE</command> and <command>EXECUTE</command> is equally
+    incorrect, since that would also leak the parameters provided to
+    <command>EXECUTE</command> to the server.
+   </para>
+
+   <para>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</programlisting>
+    Higher-level client libraries might use the protocol-level prepared
+    statements automatically and thus won't require any code changes.
+   </para>
+
+   <para>
+    <application>psql</application> provides the command
+    <literal>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   The steps to set up automatic client-side column-level encryption for a
+   database are:
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK, for example, using a cryptographic
+      library or toolkit, or a key management system.  Secure access to the
+      key as appropriate, using access control, passwords, etc.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database using the SQL command <xref
+      linkend="sql-create-column-master-key"/>.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the (unencrypted) key material for the CEK in a temporary
+      location.  (It will be encrypted in the next step.  Depending on the
+      available tools, it might be possible and sensible to combine these two
+      steps.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material using the CMK (created earlier).
+      (The unencrypted version of the CEK key material can now be disposed
+      of.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database using the SQL command <xref
+      linkend="sql-create-column-encryption-key"/>.  This command
+      <quote>uploads</quote> the encrypted CEK key material created in the
+      previous step to the database server.  The local copy of the CEK key
+      material can then be removed.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns using the created CEK.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the client library/driver to be able to look up the CMK
+      created earlier.
+     </para>
+    </listitem>
+   </orderedlist>
+
+   Once this is done, values can be written to and read from the encrypted
+   columns in a transparent way.
+  </para>
+
+  <para>
+   Note that these steps should not be run on the database server, but on some
+   client machine.  Neither the CMK nor the unencrypted CEK should ever appear
+   on the database server host.
+  </para>
+
+  <para>
+   The specific details of this setup depend on the desired CMK storage
+   mechanism/key management system as well as the client libraries to be used.
+   The following example uses the <command>openssl</command> command-line tool
+   to set up the keys.
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK and write it to a file:
+<programlisting>
+openssl genpkey -algorithm rsa -out cmk1.pem
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database:
+<programlisting>
+psql ... -c "CREATE COLUMN MASTER KEY cmk1"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</programlisting>
+      (See <xref linkend="protocol-cek-table"/> for required key lengths.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material:
+<programlisting>
+openssl pkeyutl -encrypt -inkey cmk1.pem -pkeyopt rsa_padding_mode:oaep -in cek1.bin -out cek1.bin.enc
+rm cek1.bin
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database:
+<programlisting>
+# convert file contents to hex encoding; this is just one possible way
+cekenchex=$(perl -0ne 'print unpack "H*"' cek1.bin.enc)
+psql ... -c "CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, encrypted_value = '\\x${cekenchex}')"
+rm cek1.bin.enc
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns as shown in the examples above.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the libpq for CMK lookup (see also <xref linkend="libpq-connect-cmklookup"/>):
+<programlisting>
+PGCMKLOOKUP="*=file:$PWD/%k.pem"
+export PGCMKLOOKUP
+</programlisting>
+     </para>
+
+     <para>
+      Additionally, libpq requires that the connection parameter <xref
+      linkend="libpq-connect-column-encryption"/> be set in order to activate
+      the automatic client-side column-level encryption functionality.  This
+      should be done in the connection parameters of the application, but an
+      environment variable (<envar>PGCOLUMNENCRYPTION</envar>) is also
+      available.
+     </para>
+    </listitem>
+   </orderedlist>
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Guidance on Using Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use automatic client-side column-level encryption, and what precautions
+    need to be taken to maintain its security.
+   </para>
+
+   <para>
+    In general, column encryption is never a replacement for additional
+    security and encryption techniques such as transport encryption
+    (SSL/TLS), storage encryption, strong access control, and password
+    security.  Column encryption only targets specific use cases and should be
+    used in conjunction with additional security measures.
+   </para>
+
+   <para>
+    A typical use case for column encryption is to encrypt specific values
+    with additional security requirements, for example credit card numbers.
+    This allows you to store that security-sensitive data together with the
+    rest of your data (thus getting various benefits, such as referential
+    integrity, consistent backups), while giving access to that data only to
+    specific clients and preventing accidental leakage on the server side
+    (server logs, file system backups, etc.).
+   </para>
+
+   <para>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation; for
+    <application>libpq</application>, see <xref
+    linkend="libpq-connect-column-encryption"/>).
+   </para>
+
+   <para>
+    Column encryption cannot hide the existence or absence of data, it can
+    only disguise the particular data that is known to exist.  For example,
+    storing a cleartext person name and an encrypted credit card number
+    indicates that the person has a credit card.  That might not reveal too
+    much if the database is for an online store and there is other data nearby
+    that shows that the person has recently made purchases.  But in another
+    example, storing a cleartext person name and an encrypted diagnosis in a
+    medical database probably indicates that the person has a medical issue.
+    Depending on the circumstances, that might not by itself be sufficient
+    security.
+   </para>
+
+   <para>
+    Encryption cannot completely hide the length of values.  The encryption
+    methods will pad values to multiples of the underlying cipher's block size
+    (usually 16 bytes), so some length differences will be unified this way.
+    There is no concern if all values are of the same length, but if there are
+    signficant length differences between valid values and that length
+    information is security-sensitive, then application-specific workarounds
+    such as padding would need to be applied.  How to do that securely is
+    beyond the scope of this manual.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <tip>
+    <para>
+     One might be inclined to think of the client-side column-level encryption
+     feature as a mechanism for application writers and users to protect
+     themselves against an <quote>evil DBA</quote>, but that is not the
+     intended purpose.  Rather, it is (also) a tool for the DBA to control
+     which data they do not want (in plaintext) on the server.
+    </para>
+   </tip>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on is usually
+     subject to government or industry regulations.  This section is not meant
+     to provide complete instructions on how to do this correctly.  Please
+     seek additional advice when engaging in such projects.
+    </para>
+   </note>
+  </sect2>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
@@ -1985,6 +2419,14 @@ <title>Privileges</title>
        server.  Grantees may also create, alter, or drop their own user
        mappings associated with that server.
       </para>
+      <para>
+       For column master keys, allows the creation of column encryption keys
+       using the master key.
+      </para>
+      <para>
+       For column encryption keys, allows the use of the key in the creation
+       of table columns.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2151,6 +2593,8 @@ <title>ACL Privilege Abbreviations</title>
       <entry><literal>USAGE</literal></entry>
       <entry><literal>U</literal></entry>
       <entry>
+       <literal>COLUMN ENCRYPTION KEY</literal>,
+       <literal>COLUMN MASTER KEY</literal>,
        <literal>DOMAIN</literal>,
        <literal>FOREIGN DATA WRAPPER</literal>,
        <literal>FOREIGN SERVER</literal>,
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 0cbdf63632..137c0e366f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -23349,6 +23349,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 7c01a541fe..818038d860 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,32 @@ <title>Glossary</title>
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-column-encryption-key">
+   <glossterm>Column encryption key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt column values when using automatic
+     client-side column-level encryption (<xref
+     linkend="ddl-column-encryption"/>).  Column encryption keys are stored in
+     the database encrypted by another key, the <glossterm
+     linkend="glossary-column-master-key">column master key</glossterm>.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt <glossterm
+     linkend="glossary-column-encryption-key">column encryption
+     keys</glossterm>.  (So the column master key is a <firstterm>key
+     encryption key</firstterm>.)  Column master keys are stored outside the
+     database system, for example in a key management system.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-commit">
    <glossterm>Commit</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 0e7ae70c70..d9069e897f 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,141 @@ <title>Parameter Key Words</title>
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-connect-column-encryption" xreflabel="column_encryption">
+      <term><literal>column_encryption</literal></term>
+      <listitem>
+       <para>
+        If set to <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, this enables automatic client-side column-level
+        encryption for the connection.  If encrypted columns are queried and
+        this is not enabled, the encrypted values are returned.  See <xref
+        linkend="ddl-column-encryption"/> for more information about this
+        feature.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-cmklookup" xreflabel="cmklookup">
+      <term><literal>cmklookup</literal></term>
+      <listitem>
+       <para>
+        This specifies how libpq should look up column master keys (CMKs) in
+        order to decrypt the column encryption keys (CEKs).
+        The value is a list of <literal>key=value</literal> entries separated
+        by semicolons.  Each key is the name of a key realm, or
+        <literal>*</literal> to match all realms.  The value is a
+        <literal>scheme:data</literal> specification.  The scheme specifies
+        the method to look up the key, the remaining data is specific to the
+        scheme.  Placeholders are replaced in the remaining data as follows:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>%a</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name (see <xref linkend="protocol-cmk-table"/>)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%r</literal></term>
+          <listitem>
+           <para>
+            The realm name
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        Available schemes are:
+        <variablelist>
+         <varlistentry>
+          <term><literal>file</literal></term>
+          <listitem>
+           <para>
+            Load the key material from a file.  The remaining data is the file
+            name.  Use this if the CMKs are kept in a file on the file system.
+           </para>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>run</literal></term>
+          <listitem>
+           <para>
+            Run the specified command to decrypt the CEK.  The remaining data
+            is a shell command.  Use this with key management systems that
+            perform the decryption themselves.  The command must print the
+            decrypted plaintext on the standard output.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        The default value is empty.
+       </para>
+
+       <para>
+        Example:
+<programlisting>
+cmklookup="r1=file:/some/where/secrets/%k.pem;*=file:/else/where/%r/%k.pem"
+</programlisting>
+        This specification says, for keys in realm <quote>r1</quote>, load
+        them from the specified file, replacing <literal>%k</literal> by the
+        key name.  For keys in other realms, load them from the file,
+        replacing realm and key names as specified.
+       </para>
+
+       <para>
+        An example for interacting with a (hypothetical) key management
+        system:
+<programlisting>
+cmklookup="*=run:acmekms decrypt --key %k --alg %a --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2999,32 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            half-byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this half-byte to one to force
+            encryption.  For example, use the C code literal
+            <literal>0x10</literal> to specify text format with forced
+            encryption.  If the array pointer is null then encryption is not
+            forced for any parameter.
+           </para>
+
+           <para>
+            Parameters corresponding to encrypted columns must be passed in
+            text format.  Specifying binary format for such a parameter will
+            result in an error.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter does not
+            correspond to an encrypted column on the server, then the call
+            will fail and the parameter will not be sent.  This can be used
+            for additional security against a compromised server.  (The
+            drawback is that application code then needs to be kept up to date
+            with knowledge about which columns are encrypted rather than
+            letting the server specify this.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3037,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3196,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Sends a request to execute a prepared statement with given
+        parameters, and waits for the result, with support for encrypted columns.
+<synopsis>
+PGresult *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> is like <xref
+        linkend="libpq-PQexecPrepared"/> with additional support for encrypted
+        columns.  The parameter <parameter>paramDesc</parameter> must be a
+        result set obtained from <xref linkend="libpq-PQdescribePrepared"/> on
+        the same prepared statement.
+       </para>
+
+       <para>
+        This function must be used if a statement parameter corresponds to an
+        underlying encrypted column.  In that situation, the prepared
+        statement needs to be described first so that libpq can obtain the
+        necessary key and other information from the server.  When that is
+        done, the parameters corresponding to encrypted columns are
+        automatically encrypted appropriately before being sent to the server.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4084,28 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQfisencrypted">
+     <term><function>PQfisencrypted</function><indexterm><primary>PQfisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given column came from an encrypted
+       column.  Column numbers start at 0.
+<synopsis>
+int PQfisencrypted(const PGresult *res,
+                   int column_number);
+</synopsis>
+      </para>
+
+      <para>
+       Encrypted column values are automatically decrypted, so this function
+       is not necessary to access the column value.  It can be used for extra
+       security to check whether the value was stored encrypted when one
+       thought it should be.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQfsize">
      <term><function>PQfsize</function><indexterm><primary>PQfsize</primary></indexterm></term>
 
@@ -4059,6 +4287,31 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQparamisencrypted">
+     <term><function>PQparamisencrypted</function><indexterm><primary>PQparamisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given parameter is destined for an
+       encrypted column.  Parameter numbers start at 0.
+<synopsis>
+int PQparamisencrypted(const PGresult *res, int param_number);
+</synopsis>
+      </para>
+
+      <para>
+       Values for parameters destined for encrypted columns are automatically
+       encrypted, so this function is not necessary to prepare the parameter
+       value.  It can be used for extra security to check whether the value
+       will be stored encrypted when one thought it should be.  (But see also
+       at <xref linkend="libpq-PQexecPreparedDescribed"/> for another way to do that.)
+       This function is only useful when inspecting the result of <xref
+       linkend="libpq-PQdescribePrepared"/>.  For other types of results it
+       will return false.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQprint">
      <term><function>PQprint</function><indexterm><primary>PQprint</primary></indexterm></term>
 
@@ -4584,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4845,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4902,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4963,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Sends a request to execute a prepared statement with given
+       parameters, without waiting for the result(s), with support for encrypted columns.
+<synopsis>
+int PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> is like <xref
+       linkend="libpq-PQsendQueryPrepared"/> with additional support for encrypted
+       columns.  The parameter <parameter>paramDesc</parameter> must be a
+       result set obtained from <xref linkend="libpq-PQsendDescribePrepared"/> on
+       the same prepared statement.
+      </para>
+
+      <para>
+       This function must be used if a statement parameter corresponds to an
+       underlying encrypted column.  In that situation, the prepared
+       statement needs to be described first so that libpq can obtain the
+       necessary key and other information from the server.  When that is
+       done, the parameters corresponding to encrypted columns are
+       automatically encrypted appropriately before being sent to the server.
+       See also under <xref linkend="libpq-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5052,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8086,26 @@ <title>Environment Variables</title>
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCMKLOOKUP</envar></primary>
+      </indexterm>
+      <envar>PGCMKLOOKUP</envar> behaves the same as the <xref
+      linkend="libpq-connect-cmklookup"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCOLUMNENCRYPTION</envar></primary>
+      </indexterm>
+      <envar>PGCOLUMNENCRYPTION</envar> behaves the same as the <xref
+      linkend="libpq-connect-column-encryption"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 93fc7167d4..52c29bf8da 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,76 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-column-encryption">
+   <title>Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    Automatic client-side column-level encryption is enabled by sending the
+    parameter <literal>_pq_.column_encryption</literal> with a value of
+    <literal>1</literal> in the StartupMessage.  This is a protocol extension
+    that enables a few additional protocol messages and adds additional fields
+    to existing protocol messages.  Client drivers should only activate this
+    protocol extension when requested by the user, for example through a
+    connection parameter.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the
+    messages ColumnMasterKey and ColumnEncryptionKey can appear before
+    RowDescription and ParameterDescription messages.  Clients should collect
+    the information in these messages and keep them for the duration of the
+    connection.  A server is not required to resend the key information for
+    each statement cycle if it was already sent during this connection.  If a
+    server resends a key that the client has already stored (that is, a key
+    having an ID equal to one already stored), the new information should
+    replace the old.  (This could happen, for example, if the key was altered
+    by server-side DDL commands.)
+   </para>
+
+   <para>
+    A client supporting automatic column-level encryption should automatically
+    decrypt the column value fields of DataRow messages corresponding to
+    encrypted columns, and it should automatically encrypt the parameter value
+    fields of Bind messages corresponding to encrypted columns.
+   </para>
+
+   <para>
+    When column encryption is used, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column-level encryption protocol option is enabled.  That way, a client
+    library only needs to support decrypting data sent in binary and does not
+    have to support decoding the text format of the encryption-related types
+    (see <xref linkend="datatype-encrypted"/>).
+   </para>
+
+   <para>
+    When deterministic encryption is used, clients need to take care to
+    represent plaintext to be encrypted in a consistent form.  For example,
+    encrypting an integer represented by the string <literal>100</literal> and
+    an integer represented by the string <literal>+100</literal> would result
+    in two different ciphertexts, thus defeating the main point of
+    deterministic encryption.  This protocol specification requires the
+    plaintext to be in <quote>canonical</quote> form, which is the form that
+    is produced by the server when it outputs a particular value in text
+    format.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the client
+    encoding must match the server encoding.  This ensures that all values
+    encrypted or decrypted by the client match the server encoding.
+   </para>
+
+   <para>
+    The cryptographic operations used for automatic client-side column-level
+    encryption are described in <xref
+    linkend="protocol-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -4061,6 +4131,140 @@ <title>Message Formats</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ColumnEncryptionKey">
+    <term>ColumnEncryptionKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('Y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column encryption key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the master key used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the algorithm used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The length of the following key material.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Byte<replaceable>n</replaceable></term>
+       <listitem>
+        <para>
+         The key material, encrypted with the master key referenced above.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="protocol-message-formats-ColumnMasterKey">
+    <term>ColumnMasterKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column master key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The key's realm.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="protocol-message-formats-CommandComplete">
     <term>CommandComplete (B)</term>
     <listitem>
@@ -5157,6 +5361,45 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each parameter:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         This is used as a bit field of flags.  If the column is
+         encrypted and bit 0x01 is set, the column uses deterministic
+         encryption, otherwise randomized encryption.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5545,6 +5788,34 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each field:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7370,6 +7641,176 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-column-encryption-crypto">
+  <title>Automatic Client-side Column-level Encryption Cryptography</title>
+
+  <para>
+   This section describes the cryptographic operations used by the automatic
+   client-side column-level encryption functionality.  A client that supports
+   this functionality needs to implement these operations as specified here in
+   order to be able to interoperate with other clients.
+  </para>
+
+  <para>
+   Column encryption key algorithms and column master key algorithms are
+   identified by integers in the protocol messages and the system catalogs.
+   Additional algorithms may be added to this protocol specification without a
+   change in the protocol version number.  Clients should implement support
+   for all the algorithms specified here.  If a client encounters an algorithm
+   identifier it does not recognize or does not support, it must raise an
+   error.  A suitable error message should be provided to the application or
+   user.
+  </para>
+
+  <sect2 id="protocol-cmk">
+   <title>Column Master Keys</title>
+
+   <para>
+    The currently defined algorithms for column master keys are listed in
+    <xref linkend="protocol-cmk-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC
+       8017</ulink>/PKCS #1)</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect2>
+
+  <sect2 id="protocol-cek">
+   <title>Column Encryption Keys</title>
+
+   <para>
+    The currently defined algorithms for column encryption keys are listed in
+    <xref linkend="protocol-cek-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</entry>
+       <entry><literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</entry>
+       <entry><literal>AEAD_AES_192_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_512</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <para>
+    The <quote>associated data</quote> in these algorithms consists of 4
+    bytes: The ASCII letters <literal>P</literal> and <literal>G</literal>
+    (byte values 80 and 71), followed by the version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </para>
+
+   <para>
+    The length of the initialization vector is 16 octets for all CEK algorithm
+    variants.  For randomized encryption, the initialization vector should be
+    (cryptographically strong) random bytes.  For deterministic encryption,
+    the initialization vector is constructed as
+<programlisting>
+SUBSTRING(<replaceable>HMAC</replaceable>(<replaceable>K</replaceable>, <replaceable>P</replaceable>) FOR <replaceable>IVLEN</replaceable>)
+</programlisting>
+    where <replaceable>HMAC</replaceable> is the HMAC function associated with
+    the algorithm, <replaceable>K</replaceable> is the IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </para>
+  </sect2>
+ </sect1>
+
  <sect1 id="protocol-changes">
   <title>Summary of Changes since Protocol 2.0</title>
 
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 54b5f22d6e..a730e5d650 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -8,6 +8,8 @@
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
+<!ENTITY alterColumnEncryptionKey SYSTEM "alter_column_encryption_key.sgml">
+<!ENTITY alterColumnMasterKey SYSTEM "alter_column_master_key.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
 <!ENTITY alterDefaultPrivileges SYSTEM "alter_default_privileges.sgml">
@@ -62,6 +64,8 @@
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
+<!ENTITY createColumnEncryptionKey SYSTEM "create_column_encryption_key.sgml">
+<!ENTITY createColumnMasterKey SYSTEM "create_column_master_key.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
 <!ENTITY createDatabase     SYSTEM "create_database.sgml">
 <!ENTITY createDomain       SYSTEM "create_domain.sgml">
@@ -109,6 +113,8 @@
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
+<!ENTITY dropColumnEncryptionKey SYSTEM "drop_column_encryption_key.sgml">
+<!ENTITY dropColumnMasterKey SYSTEM "drop_column_master_key.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
 <!ENTITY dropDatabase       SYSTEM "drop_database.sgml">
 <!ENTITY dropDomain         SYSTEM "drop_domain.sgml">
diff --git a/doc/src/sgml/ref/alter_column_encryption_key.sgml b/doc/src/sgml/ref/alter_column_encryption_key.sgml
new file mode 100644
index 0000000000..655e1e00d8
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+doc/src/sgml/ref/alter_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-encryption-key">
+ <indexterm zone="sql-alter-column-encryption-key">
+  <primary>ALTER COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>change the definition of a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> ADD VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> DROP VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN ENCRYPTION KEY</command> changes the definition of a
+   column encryption key.
+  </para>
+
+  <para>
+   The first form adds new encrypted key data to a column encryption key,
+   which must be encrypted with a different column master key than the
+   existing key data.  The second form removes a key data entry for a given
+   column master key.  Together, these forms can be used for column master key
+   rotation.
+  </para>
+
+  <para>
+   You must own the column encryption key to use <command>ALTER COLUMN
+   ENCRYPTION KEY</command>.  To alter the owner, you must also be a direct or
+   indirect member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column encryption key's
+   schema.  (These restrictions enforce that altering the owner doesn't do
+   anything you couldn't do by dropping and recreating the column encryption
+   key.  However, a superuser can alter ownership of any column encryption key
+   anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column encryption
+      key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  See <xref
+      linkend="sql-create-column-encryption-key"/> for details
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rotate the master keys used to encrypt a given column encryption key,
+   use a command sequence like this:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (
+    COLUMN_MASTER_KEY = cmk1
+);
+</programlisting>
+  </para>
+
+  <para>
+   To rename the column encryption key <literal>cek1</literal> to
+   <literal>cek2</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column encryption key <literal>cek1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN ENCRYPTION KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/alter_column_master_key.sgml b/doc/src/sgml/ref/alter_column_master_key.sgml
new file mode 100644
index 0000000000..7f0e656ef0
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+doc/src/sgml/ref/alter_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-master-key">
+ <indexterm zone="sql-alter-column-master-key">
+  <primary>ALTER COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN MASTER KEY</refname>
+  <refpurpose>change the definition of a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> ( REALM = <replaceable>realm</replaceable> )
+
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </para>
+
+  <para>
+   You must own the column master key to use <command>ALTER COLUMN MASTER
+   KEY</command>.  To alter the owner, you must also be a direct or indirect
+   member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column master key's schema.
+   (These restrictions enforce that altering the owner doesn't do anything you
+   couldn't do by dropping and recreating the column master key.  However, a
+   superuser can alter ownership of any column master key anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the column master key <literal>cmk1</literal> to
+   <literal>cmk2</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column master key <literal>cmk1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN MASTER KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..1caf9bfa56 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..ebcbf5d00a 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,16 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support automatic client-side
+    column-level encryption or decryption; its input or output data will
+    always be the ciphertext.  This is usually suitable for backups (see also
+    <xref linkend="app-pgdump"/>).  If automatic client-side encryption or
+    decryption is wanted, <command>INSERT</command> and
+    <command>SELECT</command> need to be used instead to write and read the
+    data.
+   </para>
+
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/ref/create_column_encryption_key.sgml b/doc/src/sgml/ref/create_column_encryption_key.sgml
new file mode 100644
index 0000000000..56fb253dad
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,170 @@
+<!--
+doc/src/sgml/ref/create_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-encryption-key">
+ <indexterm zone="sql-create-column-encryption-key">
+  <primary>CREATE COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>define a new column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN ENCRYPTION KEY <replaceable>name</replaceable> WITH VALUES (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+[ , ... ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN ENCRYPTION KEY</command> defines a new column
+   encryption key.  A column encryption key is used for client-side encryption
+   of table columns that have been defined as encrypted.  The key material of
+   a column encryption key is stored in the database's system catalogs,
+   encrypted (wrapped) by a column master key (which in turn is only
+   accessible to the client, not the database server).
+  </para>
+
+  <para>
+   A column encryption key can be associated with more than one column master
+   key.  To specify that, specify more than one parenthesized definition (see
+   also the examples).
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column encryption key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.  You must have <literal>USAGE</literal> privilege on the
+      column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  Supported algorithms are:
+      <itemizedlist>
+       <listitem>
+        <para><literal>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></para>
+       </listitem>
+      </itemizedlist>
+     </para>
+
+     <para>
+      This is informational only.  The specified value is provided to the
+      client, which may use it for decrypting the column encryption key on the
+      client side.  But a client is also free to ignore this information and
+      figure out how to arrange the decryption in some other way.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+</programlisting>
+  </para>
+
+  <para>
+   To specify more than one associated column master key:
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\xF1F2F2F4...'
+);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_column_master_key.sgml b/doc/src/sgml/ref/create_column_master_key.sgml
new file mode 100644
index 0000000000..6aaa1088d1
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+doc/src/sgml/ref/create_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-master-key">
+ <indexterm zone="sql-create-column-master-key">
+  <primary>CREATE COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN MASTER KEY</refname>
+  <refpurpose>define a new column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN MASTER KEY <replaceable>name</replaceable> [ WITH (
+    [ REALM = <replaceable>realm</replaceable> ]
+) ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN MASTER KEY</command> defines a new column master
+   key.  A column master key is used to encrypt column encryption keys, which
+   are the keys that actually encrypt the column data.  The key material of
+   the column master key is not stored in the database.  The definition of a
+   column master key records information that will allow a client to locate
+   the key material, for example in a file or in a key management system.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column master key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>realm</replaceable></term>
+
+    <listitem>
+     <para>
+      This is an optional string that can be used to organize column master
+      keys into groups for lookup by clients.  The intent is that all column
+      master keys that are stored in the same system (file system location,
+      key management system, etc.) should be in the same realm.  A client
+      would then be configured to look up all keys in a given realm in a
+      certain way.  See the documentation of the respective client library for
+      further usage instructions.
+     </para>
+
+     <para>
+      The default is the empty string.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+<programlisting>
+CREATE COLUMN MASTER KEY cmk1 (realm = 'myrealm');
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index a03dee4afe..d1549c7f45 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -22,7 +22,7 @@
  <refsynopsisdiv>
 <synopsis>
 CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable> ( [
-  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,47 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables automatic client-side column-level encryption for the column.
+      <replaceable>encryption_options</replaceable> are comma-separated
+      <literal>key=value</literal> specifications.  The following options are
+      available:
+      <variablelist>
+       <varlistentry>
+        <term><literal>column_encryption_key</literal></term>
+        <listitem>
+         <para>
+          Specifies the name of the column encryption key to use.  Specifying
+          this is mandatory.  You must have <literal>USAGE</literal> privilege
+          on the column encryption key.
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>encryption_type</literal></term>
+        <listitem>
+         <para>
+          <literal>randomized</literal> (the default) or <literal>deterministic</literal>
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>algorithm</literal></term>
+        <listitem>
+         <para>
+          The encryption algorithm to use.  The default is
+          <literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -704,6 +745,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</literal></term>
         <listitem>
diff --git a/doc/src/sgml/ref/drop_column_encryption_key.sgml b/doc/src/sgml/ref/drop_column_encryption_key.sgml
new file mode 100644
index 0000000000..f2ac1beb08
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_encryption_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-encryption-key">
+ <indexterm zone="sql-drop-column-encryption-key">
+  <primary>DROP COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>remove a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN ENCRYPTION KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN ENCRYPTION KEY</command> removes a previously defined
+   column encryption key.  To be able to drop a column encryption key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column encryption key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column encryption key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column encryption key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column encryption key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN ENCRYPTION KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_column_master_key.sgml b/doc/src/sgml/ref/drop_column_master_key.sgml
new file mode 100644
index 0000000000..fae95e09d1
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_master_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-master-key">
+ <indexterm zone="sql-drop-column-master-key">
+  <primary>DROP COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN MASTER KEY</refname>
+  <refpurpose>remove a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN MASTER KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN MASTER KEY</command> removes a previously defined
+   column master key.  To be able to drop a column master key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column master key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column master key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column master key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column master key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN MASTER KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 35bf0332c8..f712f8e9e4 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -46,6 +46,16 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN ENCRYPTION KEY <replaceable>cek_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN MASTER KEY <replaceable>cmk_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON DOMAIN <replaceable>domain_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -513,7 +523,7 @@ <title>Compatibility</title>
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
+    Privileges on databases, tablespaces, schemas, keys, languages, and
     configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 2c938cd7e1..bb2e12148c 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -715,6 +715,48 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option turns on the column encryption connection option in
+        <application>libpq</application> (see <xref
+        linkend="libpq-connect-column-encryption"/>).  Column master key
+        lookup must be configured by the user, either through a connection
+        option or an environment setting (see <xref
+        linkend="libpq-connect-cmklookup"/>).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  (But then it is recommended
+        to not do this on the same host as the server, to avoid exposing
+        unencrypted data that is meant to be kept encrypted on the server.)
+        Note that a dump created with this option cannot be restored into a
+        database with column encryption.
+       </para>
+       <!--
+           XXX: The latter would require another pg_dump option to put out
+           INSERT ... \bind ... \g commands.
+       -->
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index e62d05e5ab..4bf60c729f 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -259,6 +259,33 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  Note that a dump created
+        with this option cannot be restored into a database with column
+        encryption.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index dc6528dc11..e68b8440be 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4026,6 +4054,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <term><varname>HIDE_COLUMN_ENCRYPTION</varname></term>
+        <listitem>
+        <para>
+         If this variable is set to <literal>true</literal>, column encryption
+         details are not displayed. This is mainly useful for regression
+         tests.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..c898997915 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -36,6 +36,8 @@ <title>SQL Commands</title>
    &abort;
    &alterAggregate;
    &alterCollation;
+   &alterColumnEncryptionKey;
+   &alterColumnMasterKey;
    &alterConversion;
    &alterDatabase;
    &alterDefaultPrivileges;
@@ -90,6 +92,8 @@ <title>SQL Commands</title>
    &createAggregate;
    &createCast;
    &createCollation;
+   &createColumnEncryptionKey;
+   &createColumnMasterKey;
    &createConversion;
    &createDatabase;
    &createDomain;
@@ -137,6 +141,8 @@ <title>SQL Commands</title>
    &dropAggregate;
    &dropCast;
    &dropCollation;
+   &dropColumnEncryptionKey;
+   &dropColumnMasterKey;
    &dropConversion;
    &dropDatabase;
    &dropDomain;
diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index ef818228ac..a00545080d 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -20,7 +20,9 @@
 
 #include "access/printsimple.h"
 #include "catalog/pg_type.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 
 /*
@@ -46,6 +48,11 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc)
 		pq_sendint16(&buf, attr->attlen);
 		pq_sendint32(&buf, attr->atttypmod);
 		pq_sendint16(&buf, 0);	/* format code */
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&buf, 0);	/* CEK */
+			pq_sendint32(&buf, 0);	/* CEK alg */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 72faeb5dfa..63a3d9b0f9 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,28 @@
  */
 #include "postgres.h"
 
+#include "access/genam.h"
 #include "access/printtup.h"
+#include "access/skey.h"
+#include "access/table.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
 #include "libpq/libpq.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memdebug.h"
 #include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
 
 
 static void printtup_startup(DestReceiver *self, int operation,
@@ -151,6 +166,156 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 	 */
 }
 
+/*
+ * Send ColumnMasterKey message, unless it's already been sent in this session
+ * for this key.
+ */
+List	   *cmk_sent = NIL;
+
+static void
+cmk_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+}
+
+static void
+MaybeSendColumnMasterKeyMessage(Oid cmkid)
+{
+	HeapTuple	tuple;
+	Form_pg_colmasterkey cmkform;
+	Datum		datum;
+	bool		isnull;
+	StringInfoData buf;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cmk_sent, cmkid))
+		return;
+
+	tuple = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	cmkform = (Form_pg_colmasterkey) GETSTRUCT(tuple);
+
+	pq_beginmessage(&buf, 'y'); /* ColumnMasterKey */
+	pq_sendint32(&buf, cmkform->oid);
+	pq_sendstring(&buf, NameStr(cmkform->cmkname));
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkrealm, &isnull);
+	Assert(!isnull);
+	pq_sendstring(&buf, TextDatumGetCString(datum));
+	pq_endmessage(&buf);
+
+	ReleaseSysCache(tuple);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CMKOID, cmk_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cmk_sent = lappend_oid(cmk_sent, cmkid);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * Send ColumnEncryptionKey message, unless it's already been sent in this
+ * session for this key.
+ */
+List	   *cek_sent = NIL;
+
+static void
+cek_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
+void
+MaybeSendColumnEncryptionKeyMessage(Oid attcek)
+{
+	HeapTuple	tuple;
+	ScanKeyData skey[1];
+	SysScanDesc sd;
+	Relation	rel;
+	bool		found = false;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cek_sent, attcek))
+		return;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	ScanKeyInit(&skey[0],
+				Anum_pg_colenckeydata_ckdcekid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(attcek));
+	rel = table_open(ColumnEncKeyDataRelationId, AccessShareLock);
+	sd = systable_beginscan(rel, ColumnEncKeyCekidCmkidIndexId, true, NULL, 1, skey);
+
+	while ((tuple = systable_getnext(sd)))
+	{
+		Form_pg_colenckeydata ckdform = (Form_pg_colenckeydata) GETSTRUCT(tuple);
+		Datum		datum;
+		bool		isnull;
+		bytea	   *ba;
+		StringInfoData buf;
+
+		MaybeSendColumnMasterKeyMessage(ckdform->ckdcmkid);
+
+		datum = heap_getattr(tuple, Anum_pg_colenckeydata_ckdencval, RelationGetDescr(rel), &isnull);
+		Assert(!isnull);
+		ba = pg_detoast_datum_packed((bytea *) DatumGetPointer(datum));
+
+		pq_beginmessage(&buf, 'Y'); /* ColumnEncryptionKey */
+		pq_sendint32(&buf, ckdform->ckdcekid);
+		pq_sendint32(&buf, ckdform->ckdcmkid);
+		pq_sendint32(&buf, ckdform->ckdcmkalg);
+		pq_sendint32(&buf, VARSIZE_ANY_EXHDR(ba));
+		pq_sendbytes(&buf, VARDATA_ANY(ba), VARSIZE_ANY_EXHDR(ba));
+		pq_endmessage(&buf);
+
+		found = true;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	systable_endscan(sd);
+	table_close(rel, NoLock);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CEKDATAOID, cek_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cek_sent = lappend_oid(cek_sent, attcek);
+	MemoryContextSwitchTo(oldcontext);
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -167,6 +332,7 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 						  List *targetlist, int16 *formats)
 {
 	int			natts = typeinfo->natts;
+	size_t		sz;
 	int			i;
 	ListCell   *tlist_item = list_head(targetlist);
 
@@ -183,14 +349,17 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 	 * Have to overestimate the size of the column-names, to account for
 	 * character set overhead.
 	 */
-	enlargeStringInfo(buf, (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
-							+ sizeof(Oid)	/* resorigtbl */
-							+ sizeof(AttrNumber)	/* resorigcol */
-							+ sizeof(Oid)	/* atttypid */
-							+ sizeof(int16) /* attlen */
-							+ sizeof(int32) /* attypmod */
-							+ sizeof(int16) /* format */
-							) * natts);
+	sz = (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
+		  + sizeof(Oid)	/* resorigtbl */
+		  + sizeof(AttrNumber)	/* resorigcol */
+		  + sizeof(Oid)	/* atttypid */
+		  + sizeof(int16) /* attlen */
+		  + sizeof(int32) /* attypmod */
+		  + sizeof(int16)); /* format */
+	if (MyProcPort->column_encryption_enabled)
+		sz += (sizeof(int32)		/* attcekid */
+			   + sizeof(int32));	/* attencalg */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +369,8 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +402,29 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(atttypid))
+		{
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(resorigtbl), Int16GetDatum(resorigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", resorigcol, resorigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			atttypid = orig_att->attusertypid;
+			atttypmod = orig_att->attusertypmod;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +432,11 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		pq_writeint16(buf, att->attlen);
 		pq_writeint32(buf, atttypmod);
 		pq_writeint16(buf, format);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_writeint32(buf, attcekid);
+			pq_writeint32(buf, attencalg);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +470,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 72a2c3d3db..f86ba299c3 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attusertypid != attr2->attusertypid)
+			return false;
+		if (attr1->attusertypmod != attr2->attusertypmod)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 24bab58499..cc48069932 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index a60107bf94..7b9575635b 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index c4232344aa..a3547c6cae 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_default_acl.h"
@@ -247,6 +249,12 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_SEQUENCE:
 			whole_mask = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			whole_mask = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			whole_mask = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			whole_mask = ACL_ALL_RIGHTS_DATABASE;
 			break;
@@ -473,6 +481,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
 			errormsg = gettext_noop("invalid privilege type %s for sequence");
 			break;
+		case OBJECT_CEK:
+			all_privileges = ACL_ALL_RIGHTS_CEK;
+			errormsg = gettext_noop("invalid privilege type %s for column encryption key");
+			break;
+		case OBJECT_CMK:
+			all_privileges = ACL_ALL_RIGHTS_CMK;
+			errormsg = gettext_noop("invalid privilege type %s for column master key");
+			break;
 		case OBJECT_DATABASE:
 			all_privileges = ACL_ALL_RIGHTS_DATABASE;
 			errormsg = gettext_noop("invalid privilege type %s for database");
@@ -597,6 +613,12 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_SEQUENCE:
 			ExecGrant_Relation(istmt);
 			break;
+		case OBJECT_CEK:
+			ExecGrant_common(istmt, ColumnEncKeyRelationId, ACL_ALL_RIGHTS_CEK, NULL);
+			break;
+		case OBJECT_CMK:
+			ExecGrant_common(istmt, ColumnMasterKeyRelationId, ACL_ALL_RIGHTS_CMK, NULL);
+			break;
 		case OBJECT_DATABASE:
 			ExecGrant_common(istmt, DatabaseRelationId, ACL_ALL_RIGHTS_DATABASE, NULL);
 			break;
@@ -676,6 +698,26 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 				objects = lappend_oid(objects, relOid);
 			}
 			break;
+		case OBJECT_CEK:
+			foreach(cell, objnames)
+			{
+				List	   *cekname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cek_oid(cekname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
+		case OBJECT_CMK:
+			foreach(cell, objnames)
+			{
+				List	   *cmkname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cmk_oid(cmkname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
 		case OBJECT_DATABASE:
 			foreach(cell, objnames)
 			{
@@ -2693,6 +2735,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("permission denied for column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("permission denied for column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -2798,6 +2846,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -2828,6 +2877,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("must be owner of aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("must be owner of column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("must be owner of column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("must be owner of collation %s");
 						break;
@@ -2938,6 +2993,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3019,6 +3075,10 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
+		case OBJECT_CEK:
+			return object_aclmask(ColumnEncKeyRelationId, object_oid, roleid, mask, how);
+		case OBJECT_CMK:
+			return object_aclmask(ColumnMasterKeyRelationId, object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
 		case OBJECT_FUNCTION:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..cab6cfd140 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -30,7 +30,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -153,6 +156,9 @@ static const Oid object_classes[] = {
 	TypeRelationId,				/* OCLASS_TYPE */
 	CastRelationId,				/* OCLASS_CAST */
 	CollationRelationId,		/* OCLASS_COLLATION */
+	ColumnEncKeyRelationId,		/* OCLASS_CEK */
+	ColumnEncKeyDataRelationId,	/* OCLASS_CEKDATA */
+	ColumnMasterKeyRelationId,	/* OCLASS_CMK */
 	ConstraintRelationId,		/* OCLASS_CONSTRAINT */
 	ConversionRelationId,		/* OCLASS_CONVERSION */
 	AttrDefaultRelationId,		/* OCLASS_DEFAULT */
@@ -1493,6 +1499,9 @@ doDeletion(const ObjectAddress *object, int flags)
 
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONVERSION:
 		case OCLASS_LANGUAGE:
 		case OCLASS_OPCLASS:
@@ -2859,6 +2868,15 @@ getObjectClass(const ObjectAddress *object)
 		case CollationRelationId:
 			return OCLASS_COLLATION;
 
+		case ColumnEncKeyRelationId:
+			return OCLASS_CEK;
+
+		case ColumnEncKeyDataRelationId:
+			return OCLASS_CEKDATA;
+
+		case ColumnMasterKeyRelationId:
+			return OCLASS_CMK;
+
 		case ConstraintRelationId:
 			return OCLASS_CONSTRAINT;
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 4f006820b8..df282c796f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypid - 1] = ObjectIdGetDatum(attrs->attusertypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypmod - 1] = Int32GetDatum(attrs->attusertypmod);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attusertypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attusertypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 14e57adee2..00f914bc5f 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -26,6 +26,8 @@
 #include "catalog/dependency.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1997,6 +1999,254 @@ OpfamilyIsVisible(Oid opfid)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4567,6 +4817,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(OpfamilyIsVisible(oid));
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2f688166e1..6c4ab9ac59 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -191,6 +194,48 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_COLLATION,
 		true
 	},
+	{
+		"column encryption key",
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		CEKOID,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		Anum_pg_colenckey_cekowner,
+		Anum_pg_colenckey_cekacl,
+		OBJECT_CEK,
+		true
+	},
+	{
+		"column encryption key data",
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		CEKDATAOID,
+		-1,
+		Anum_pg_colenckeydata_oid,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		-1,
+		false
+	},
+	{
+		"column master key",
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		CMKOID,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		Anum_pg_colmasterkey_cmkowner,
+		Anum_pg_colmasterkey_cmkacl,
+		OBJECT_CMK,
+		true
+	},
 	{
 		"constraint",
 		ConstraintRelationId,
@@ -723,6 +768,18 @@ static const struct object_type_map
 	{
 		"collation", OBJECT_COLLATION
 	},
+	/* OCLASS_CEK */
+	{
+		"column encryption key", OBJECT_CEK
+	},
+	/* OCLASS_CEKDATA */
+	{
+		"column encryption key data", OBJECT_CEKDATA
+	},
+	/* OCLASS_CMK */
+	{
+		"column master key", OBJECT_CMK
+	},
 	/* OCLASS_CONSTRAINT */
 	{
 		"table constraint", OBJECT_TABCONSTRAINT
@@ -1029,6 +1086,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1108,6 +1175,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, castNode(List, object));
+					Oid			cekid;
+					Oid			cmkid;
+
+					cekid = get_cek_oid(cekname, missing_ok);
+					cmkid = get_cmk_oid(cmkname, missing_ok);
+
+					address.classId = ColumnEncKeyDataRelationId;
+					address.objectId = get_cekdata_oid(cekid, cmkid, missing_ok);
+					address.objectSubId = 0;
+				}
+				break;
 			case OBJECT_TRANSFORM:
 				{
 					TypeName   *typename = linitial_node(TypeName, castNode(List, object));
@@ -2311,6 +2393,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2367,6 +2451,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2489,6 +2574,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2575,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3040,6 +3128,92 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CEKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata cekdata;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+
+				cekdata = (Form_pg_colenckeydata) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("column encryption key data of %s for %s"),
+								 getObjectDescription(&(ObjectAddress){ColumnEncKeyRelationId, cekdata->ckdcekid}, false),
+								 getObjectDescription(&(ObjectAddress){ColumnMasterKeyRelationId, cekdata->ckdcmkid}, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4440,6 +4614,18 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "collation");
 			break;
 
+		case OCLASS_CEK:
+			appendStringInfoString(&buffer, "column encryption key");
+			break;
+
+		case OCLASS_CEKDATA:
+			appendStringInfoString(&buffer, "column encryption key data");
+			break;
+
+		case OCLASS_CMK:
+			appendStringInfoString(&buffer, "column master key");
+			break;
+
 		case OCLASS_CONSTRAINT:
 			getConstraintTypeDescription(&buffer, object->objectId,
 										 missing_ok);
@@ -4905,6 +5091,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata form;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckeydata) GETSTRUCT(tup);
+				appendStringInfo(&buffer,
+								 "of %s for %s",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column master key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91..69f6175c60 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -19,6 +19,7 @@ OBJS = \
 	analyze.o \
 	async.o \
 	cluster.o \
+	colenccmds.o \
 	collationcmds.o \
 	comment.o \
 	constraint.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index bea51b3af1..59c23e9ef8 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -22,7 +22,9 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_foreign_data_wrapper.h"
@@ -118,6 +120,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -379,6 +387,8 @@ ExecRenameStmt(RenameStmt *stmt)
 			return RenameType(stmt);
 
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_EVENT_TRIGGER:
@@ -527,6 +537,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -643,6 +655,9 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 			break;
 
 		case OCLASS_CAST:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
@@ -876,6 +891,8 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/colenccmds.c b/src/backend/commands/colenccmds.c
new file mode 100644
index 0000000000..3ada6d5aeb
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,439 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/colenccmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+static void
+parse_cek_attributes(ParseState *pstate, List *definition, Oid *cmkoid_p, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	Assert(cmkoid_p);
+
+	foreach(lc, definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "column_master_key") == 0)
+			defelp = &cmkEl;
+		else if (strcmp(defel->defname, "algorithm") == 0)
+			defelp = &algEl;
+		else if (strcmp(defel->defname, "encrypted_value") == 0)
+			defelp = &encvalEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column encryption key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (cmkEl)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("attribute \"%s\" must be specified",
+						"column_master_key")));
+
+	if (algEl)
+	{
+		char	   *val = defGetString(algEl);
+
+		if (!alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"algorithm")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	if (encvalEl)
+	{
+		if (!encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"encrypted_value")));
+
+		encval = defGetString(encvalEl);
+	}
+	else
+	{
+		if (encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"encrypted_value")));
+	}
+
+	*cmkoid_p = cmkoid;
+	if (alg_p)
+		*alg_p = alg;
+	if (encval_p)
+		*encval_p = encval;
+}
+
+static void
+insert_cekdata_record(Oid cekoid, Oid cmkoid, int alg, char *encval)
+{
+	Oid			cekdataoid;
+	Relation	rel;
+	Datum		values[Natts_pg_colenckeydata] = {0};
+	bool		nulls[Natts_pg_colenckeydata] = {0};
+	HeapTuple	tup;
+	ObjectAddress myself;
+	ObjectAddress other;
+
+	rel = table_open(ColumnEncKeyDataRelationId, RowExclusiveLock);
+
+	cekdataoid = GetNewOidWithIndex(rel, ColumnEncKeyDataOidIndexId, Anum_pg_colenckeydata_oid);
+	values[Anum_pg_colenckeydata_oid - 1] = ObjectIdGetDatum(cekdataoid);
+	values[Anum_pg_colenckeydata_ckdcekid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckeydata_ckdcmkid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colenckeydata_ckdcmkalg - 1] = Int32GetDatum(alg);
+	values[Anum_pg_colenckeydata_ckdencval - 1] = DirectFunctionCall1(byteain, CStringGetDatum(encval));
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyDataRelationId, cekdataoid);
+
+	/* dependency cekdata -> cek */
+	ObjectAddressSet(other, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_AUTO);
+
+	/* dependency cekdata -> cmk */
+	ObjectAddressSet(other, ColumnMasterKeyRelationId, cmkoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_NORMAL);
+
+	table_close(rel, NoLock);
+}
+
+ObjectAddress
+CreateCEK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+	nulls[Anum_pg_colenckey_cekacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOnOwner(ColumnEncKeyRelationId, cekoid, GetUserId());
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnEncKeyRelationId, cekoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt)
+{
+	Oid			cekoid;
+	ObjectAddress address;
+
+	cekoid = get_cek_oid(stmt->cekname, false);
+
+	if (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(stmt->cekname));
+
+	if (stmt->isDrop)
+	{
+		Oid			cmkoid = 0;
+		Oid			cekdataoid;
+		ObjectAddress obj;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, NULL, NULL);
+		cekdataoid = get_cekdata_oid(cekoid, cmkoid, false);
+		ObjectAddressSet(obj, ColumnEncKeyDataRelationId, cekdataoid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+		AclResult	aclresult;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		if (get_cekdata_oid(cekoid, cmkoid, true))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("column encryption key \"%s\" already has data for master key \"%s\"",
+						   NameListToString(stmt->cekname), get_cmk_name(cmkoid, false)));
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	InvokeObjectPostAlterHook(ColumnEncKeyRelationId, cekoid, 0);
+	ObjectAddressSet(address, ColumnEncKeyRelationId, cekoid);
+
+	return address;
+}
+
+ObjectAddress
+CreateCMK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+		realm = defGetString(realmEl);
+	else
+		realm = "";
+
+	cmkoid = GetNewOidWithIndex(rel, ColumnMasterKeyOidIndexId, Anum_pg_colmasterkey_oid);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+	nulls[Anum_pg_colmasterkey_cmkacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	recordDependencyOnOwner(ColumnMasterKeyRelationId, cmkoid, GetUserId());
+
+	ObjectAddressSet(myself, ColumnMasterKeyRelationId, cmkoid);
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d6c6d514f3..9685b7886a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 82bda15889..b4c681005a 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +517,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..4c1628cf7b 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,9 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -1027,6 +1030,9 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_TYPE:
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_CONVERSION:
 		case OCLASS_DEFAULT:
@@ -2056,6 +2062,9 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2139,6 +2148,9 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 42cced9ebe..4b5ac30441 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..93d61c96ad 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,9 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 62d9917ca3..c30b59265b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,16 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			AclResult	aclresult;
+
+			GetColumnEncryption(colDef->encryption, attr);
+			aclresult = object_aclcheck(ColumnEncKeyRelationId, attr->attcek, GetUserId(), ACL_USAGE);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attr->attcek, false));
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -2562,13 +2575,43 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				Oid			defCollId;
 
 				/*
-				 * Yes, try to merge the two column definitions. They must
-				 * have the same type, typmod, and collation.
+				 * Yes, try to merge the two column definitions.
 				 */
 				ereport(NOTICE,
 						(errmsg("merging multiple inherited definitions of column \"%s\"",
 								attributeName)));
 				def = (ColumnDef *) list_nth(inhSchema, exist_attno - 1);
+
+				/*
+				 * Check encryption parameter.  All parents must have the same
+				 * encryption settings for a column.
+				 */
+				if ((def->encryption && !attribute->attcek) ||
+					(!def->encryption && attribute->attcek))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && attribute->attcek)
+				{
+					/*
+					 * Merging the encryption properties of two encrypted
+					 * parent columns is not yet implemented.  Right now, this
+					 * would confuse the checks of the type etc. below (we
+					 * must check the physical and the real types against each
+					 * other, respectively), which might require a larger
+					 * restructuring.  For now, just give up here.
+					 */
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("multiple inheritance of encrypted columns is not implemented")));
+				}
+
+				/*
+				 * Must have the same type, typmod, and collation.
+				 */
 				typenameTypeIdAndMod(NULL, def->typeName, &defTypeId, &deftypmod);
 				if (defTypeId != attribute->atttypid ||
 					deftypmod != attribute->atttypmod)
@@ -2641,6 +2684,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+														attribute->attusertypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -2919,6 +2968,34 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								 errdetail("%s versus %s", def->compression, newdef->compression)));
 				}
 
+				/*
+				 * Check encryption parameter.  All parents and children must
+				 * have the same encryption settings for a column.
+				 */
+				if ((def->encryption && !newdef->encryption) ||
+					(!def->encryption && newdef->encryption))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && newdef->encryption)
+				{
+					FormData_pg_attribute a, newa;
+
+					GetColumnEncryption(def->encryption, &a);
+					GetColumnEncryption(newdef->encryption, &newa);
+
+					if (a.atttypid != newa.atttypid ||
+						a.atttypmod != newa.atttypmod ||
+						a.attcek != newa.attcek)
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" has an encryption specification conflict",
+										attributeName)));
+				}
+
 				/* Mark the column as locally defined */
 				def->is_local = true;
 				/* Merge of NOT NULL constraints = OR 'em together */
@@ -6861,6 +6938,19 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attislocal = colDef->is_local;
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
+	if (colDef->encryption)
+	{
+		GetColumnEncryption(colDef->encryption, &attribute);
+		aclresult = object_aclcheck(ColumnEncKeyRelationId, attribute.attcek, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attribute.attcek, false));
+	}
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attusertypid = 0;
+		attribute.attusertypmod = -1;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12692,6 +12782,9 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
 			case OCLASS_COLLATION:
+			case OCLASS_CEK:
+			case OCLASS_CEKDATA:
+			case OCLASS_CMK:
 			case OCLASS_CONVERSION:
 			case OCLASS_LANGUAGE:
 			case OCLASS_LARGEOBJECT:
@@ -19291,3 +19384,110 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	List	   *cek = NULL;
+	Oid			cekoid;
+	bool		encdet = false;
+	int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+	foreach(lc, coldefencryption)
+	{
+		DefElem    *el = lfirst_node(DefElem, lc);
+
+		if (strcmp(el->defname, "column_encryption_key") == 0)
+			cek = defGetQualifiedName(el);
+		else if (strcmp(el->defname, "encryption_type") == 0)
+		{
+			char	   *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+			if (strcmp(val, "deterministic") == 0)
+				encdet = true;
+			else if (strcmp(val, "randomized") == 0)
+				encdet = false;
+			else
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption type: %s", val));
+		}
+		else if (strcmp(el->defname, "algorithm") == 0)
+		{
+			char	   *val = strVal(el->arg);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption algorithm: %s", val));
+		}
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized column encryption parameter: %s", el->defname));
+	}
+
+	if (!cek)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+				errmsg("column encryption key must be specified"));
+
+	cekoid = get_cek_oid(cek, false);
+
+	attr->attcek = cekoid;
+	attr->attusertypid = attr->atttypid;
+	attr->attusertypmod = attr->atttypmod;
+
+	/* override physical type */
+	if (encdet)
+		attr->atttypid = PG_ENCRYPTED_DETOID;
+	else
+		attr->atttypid = PG_ENCRYPTED_RNDOID;
+	get_typlenbyvalalign(attr->atttypid,
+						 &attr->attlen, &attr->attbyval, &attr->attalign);
+	attr->attstorage = get_typstorage(attr->atttypid);
+	attr->attcollation = InvalidOid;
+
+	attr->atttypmod = alg;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	List	   *result;
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	char	   *nspname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attr->attcek));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attr->attcek);
+
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+	nspname = get_namespace_name(form->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(form->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->atttypmod))),
+									-1));
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index bb0f5de4c2..cec2daa9c3 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -25,6 +25,7 @@
 #include "access/xlogprefetcher.h"
 #include "catalog/pg_authid.h"
 #include "common/string.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "postmaster/postmaster.h"
@@ -706,7 +707,11 @@ check_client_encoding(char **newval, void **extra, GucSource source)
 	 */
 	if (PrepareClientEncoding(encoding) < 0)
 	{
-		if (IsTransactionState())
+		if (MyProcPort->column_encryption_enabled)
+			GUC_check_errdetail("Conversion between %s and %s is not possible when column encryption is enabled.",
+								canonical_name,
+								GetDatabaseEncodingName());
+		else if (IsTransactionState())
 		{
 			/* Must be a genuine no-such-conversion problem */
 			GUC_check_errcode(ERRCODE_FEATURE_NOT_SUPPORTED);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index ff98c773f5..171dca3803 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index dc8415a693..c81bc15128 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3983,6 +3983,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(coldef->compression))
 					return true;
+				if (WALK(coldef->encryption))
+					return true;
 				if (WALK(coldef->raw_default))
 					return true;
 				if (WALK(coldef->collClause))
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a0138382a1..9f4b1e7d94 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,6 +420,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <list>	parse_toplevel stmtmulti routine_body_stmt_list
 				OptTableElementList TableElementList OptInherit definition
+				list_of_definitions
 				OptTypedTableElementList TypedTableElementList
 				reloptions opt_reloptions
 				OptWith opt_definition func_args func_args_list
@@ -592,6 +594,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableConstraint TableLikeClause
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression column_storage opt_column_storage
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_match
@@ -690,8 +693,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
 	DOUBLE_P DROP
 
-	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
-	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EACH ELSE ENABLE_P ENCODING ENCRYPTION ENCRYPTED END_P ENUM_P ESCAPE
+	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -714,7 +717,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
-	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+	MAPPING MASTER MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
 
 	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -3700,14 +3705,15 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_storage opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_encryption opt_column_storage opt_column_compression create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 
 					n->colname = $1;
 					n->typeName = $2;
-					n->storage_name = $3;
-					n->compression = $4;
+					n->encryption = $3;
+					n->storage_name = $4;
+					n->compression = $5;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3716,8 +3722,8 @@ columnDef:	ColId Typename opt_column_storage opt_column_compression create_gener
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $5;
-					SplitColQualList($6, &n->constraints, &n->collClause,
+					n->fdwoptions = $6;
+					SplitColQualList($7, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *) n;
@@ -3774,6 +3780,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4045,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6282,33 @@ DefineStmt:
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+			| CREATE COLUMN ENCRYPTION KEY any_name WITH VALUES list_of_definitions
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CEK;
+					n->defnames = $5;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name WITH definition
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = $7;
+					$$ = (Node *) n;
+				}
 		;
 
 definition: '(' def_list ')'						{ $$ = $2; }
@@ -6289,6 +6328,10 @@ def_elem:	ColLabel '=' def_arg
 				}
 		;
 
+list_of_definitions: definition						{ $$ = list_make1($1); }
+			| list_of_definitions ',' definition	{ $$ = lappend($1, $3); }
+		;
+
 /* Note: any simple identifier will be returned as a type name! */
 def_arg:	func_type						{ $$ = (Node *) $1; }
 			| reserved_keyword				{ $$ = (Node *) makeString(pstrdup($1)); }
@@ -6800,6 +6843,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -7611,6 +7656,24 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| COLUMN ENCRYPTION KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CEK;
+					n->objs = $4;
+					$$ = n;
+				}
+			| COLUMN MASTER KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CMK;
+					n->objs = $4;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9140,6 +9203,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9817,6 +9900,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10148,6 +10251,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11304,6 +11425,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_name ADD_P VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = false;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN ENCRYPTION KEY any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16958,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16854,6 +17022,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17506,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17425,6 +17595,7 @@ bare_label_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index 2240284f21..a8a1855aa0 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,159 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			Expr	   *expr = te->expr;
+
+			/*
+			 * If it's a RelabelType, look inside.  (For encrypted columns,
+			 * this would typically be a typmod adjustment.)
+			 */
+			if (IsA(expr, RelabelType))
+				expr = castNode(RelabelType, expr)->arg;
+
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(expr, Param))
+			{
+				Param	   *p = (Param *) expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(expr, Var))
+			{
+				Var		   *v = (Var *) expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell   *lc;
+
+					foreach(lc, srcrte->values_lists)
+					{
+						List	   *values_list = lfirst_node(List, lc);
+						Expr	   *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, RelabelType))
+							value = castNode(RelabelType, value)->arg;
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach(lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f9218f48aa..8d902292cd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1035,8 +1035,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+												attribute->attusertypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2552327d90..79ec9c4d1f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2210,12 +2210,27 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 									valptr),
 							 errhint("Valid values are: \"false\", 0, \"true\", 1, \"database\".")));
 			}
+			else if (strcmp(nameptr, "_pq_.column_encryption") == 0)
+			{
+				/*
+				 * Right now, the only accepted value is "1".  This gives room
+				 * to expand this into a version number, for example.
+				 */
+				if (strcmp(valptr, "1") == 0)
+					port->column_encryption_enabled = true;
+				else
+					ereport(FATAL,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for parameter \"%s\": \"%s\"",
+									"column_encryption",
+									valptr),
+							 errhint("Valid values are: 1.")));
+			}
 			else if (strncmp(nameptr, "_pq_.", 5) == 0)
 			{
 				/*
 				 * Any option beginning with _pq_. is reserved for use as a
-				 * protocol-level option, but at present no such options are
-				 * defined.
+				 * protocol-level option.
 				 */
 				unrecognized_protocol_options =
 					lappend(unrecognized_protocol_options, pstrdup(nameptr));
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index cab709b07b..c5acdd0dc2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -71,6 +72,7 @@
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
 
@@ -1815,6 +1817,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (type_is_encrypted(ptype))
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter $%d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2560,6 +2572,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2618,11 +2632,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	for (int i = 0; i < psrc->num_params; i++)
 	{
 		Oid			ptype = psrc->param_types[i];
+		Oid			pcekid = InvalidOid;
+		int			pcekalg = 0;
+		int16		pflags = 0;
+
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(ptype))
+		{
+			Oid			porigtbl = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("parameter $%d corresponds to an encrypted column, but an underlying table and column could not be determined", i + 1)));
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(porigtbl), Int16GetDatum(porigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", porigcol, porigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			ptype = orig_att->attusertypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x01;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&row_description_buf, (int) pcekid);
+			pq_sendint32(&row_description_buf, pcekalg);
+			pq_sendint16(&row_description_buf, pflags);
+		}
 	}
 	pq_endmessage_reuse(&row_description_buf);
 
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index c7d9d96b45..0a17eb4486 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -30,6 +30,7 @@
 #include "commands/alter.h"
 #include "commands/async.h"
 #include "commands/cluster.h"
+#include "commands/colenccmds.h"
 #include "commands/collationcmds.h"
 #include "commands/comment.h"
 #include "commands/conversioncmds.h"
@@ -137,6 +138,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,14 @@ ProcessUtilitySlow(ParseState *pstate,
 															stmt->definition,
 															&secondaryObject);
 							break;
+						case OBJECT_CEK:
+							Assert(stmt->args == NIL);
+							address = CreateCEK(pstate, stmt);
+							break;
+						case OBJECT_CMK:
+							Assert(stmt->args == NIL);
+							address = CreateCMK(pstate, stmt);
+							break;
 						case OBJECT_COLLATION:
 							Assert(stmt->args == NIL);
 							address = DefineCollation(pstate,
@@ -1903,6 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,12 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_COLUMN:
 			tag = CMDTAG_ALTER_TABLE;
 			break;
+		case OBJECT_CEK:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+		case OBJECT_CMK:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
 		case OBJECT_CONVERSION:
 			tag = CMDTAG_ALTER_CONVERSION;
 			break;
@@ -2640,6 +2665,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_DROP_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_DROP_COLUMN_MASTER_KEY;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2760,6 +2791,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_COLLATION:
 					tag = CMDTAG_CREATE_COLLATION;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_CREATE_COLUMN_MASTER_KEY;
+					break;
 				case OBJECT_ACCESS_METHOD:
 					tag = CMDTAG_CREATE_ACCESS_METHOD;
 					break;
@@ -3063,6 +3100,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3733,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 8f7522d103..b934f16074 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -800,6 +800,14 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			/* for backwards compatibility, grant some rights by default */
 			world_default = ACL_CREATE_TEMP | ACL_CONNECT;
@@ -911,6 +919,12 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 's':
 			objtype = OBJECT_SEQUENCE;
 			break;
+		case 'Y':
+			objtype = OBJECT_CEK;
+			break;
+		case 'y':
+			objtype = OBJECT_CMK;
+			break;
 		case 'd':
 			objtype = OBJECT_DATABASE;
 			break;
@@ -2915,6 +2929,10 @@ convert_column_priv_string(text *priv_type_text)
 }
 
 
+// TODO: has_column_encryption_key_privilege variants
+// TODO: has_column_master_key_privilege variants
+
+
 /*
  * has_database_privilege variants
  *		These are all named "has_database_privilege" at the SQL level.
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 170b3a3820..faf5a68849 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -679,6 +679,112 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c07382051d..7ad159110f 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -24,7 +24,10 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_namespace.h"
@@ -2658,6 +2661,25 @@ type_is_multirange(Oid typid)
 	return (get_typtype(typid) == TYPTYPE_MULTIRANGE);
 }
 
+bool
+type_is_encrypted(Oid typid)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+		bool		result;
+
+		result = (typtup->typcategory == TYPCATEGORY_ENCRYPTED);
+		ReleaseSysCache(tp);
+		return result;
+	}
+	else
+		return false;
+}
+
 /*
  * get_type_category_preferred
  *
@@ -3683,3 +3705,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+char *
+get_cek_name(Oid cekid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cekname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+		return NULL;
+	}
+
+	cekname = pstrdup(NameStr(((Form_pg_colenckey) GETSTRUCT(tup))->cekname));
+
+	ReleaseSysCache(tup);
+
+	return cekname;
+}
+
+Oid
+get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok)
+{
+	Oid			cekdataid;
+
+	cekdataid = GetSysCacheOid2(CEKDATACEKCMK, Anum_pg_colenckeydata_oid,
+								ObjectIdGetDatum(cekid),
+								ObjectIdGetDatum(cmkid));
+	if (!OidIsValid(cekdataid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" has no data for master key \"%s\"",
+						get_cek_name(cekid, false), get_cmk_name(cmkid, false))));
+
+	return cekdataid;
+}
+
+char *
+get_cmk_name(Oid cmkid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cmkname;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+		return NULL;
+	}
+
+	cmkname = pstrdup(NameStr(((Form_pg_colmasterkey) GETSTRUCT(tup))->cmkname));
+
+	ReleaseSysCache(tup);
+
+	return cmkname;
+}
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 77c2ba3f8f..d40af13efe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 94abede512..eb432260da 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -219,6 +222,32 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAMENSP] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameNspIndexId,
+		KEY(Anum_pg_colenckey_cekname,
+			Anum_pg_colenckey_ceknamespace),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +262,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAMENSP] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameNspIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname,
+			Anum_pg_colmasterkey_cmknamespace),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 033647011b..4b6be68f27 100644
--- a/src/backend/utils/mb/mbutils.c
+++ b/src/backend/utils/mb/mbutils.c
@@ -36,7 +36,9 @@
 
 #include "access/xact.h"
 #include "catalog/namespace.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
@@ -130,6 +132,12 @@ PrepareClientEncoding(int encoding)
 		encoding == PG_SQL_ASCII)
 		return 0;
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	if (IsTransactionState())
 	{
 		/*
@@ -237,6 +245,12 @@ SetClientEncoding(int encoding)
 		return 0;
 	}
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	/*
 	 * Search the cache for the entry previously prepared by
 	 * PrepareClientEncoding; if there isn't one, we lose.  While at it,
@@ -297,7 +311,9 @@ InitializeClientEncoding(void)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("conversion between %s and %s is not supported",
 						pg_enc2name_tbl[pending_client_encoding].name,
-						GetDatabaseEncodingName())));
+						GetDatabaseEncodingName()),
+				 (MyProcPort->column_encryption_enabled) ?
+				 errdetail("Encoding conversion is not possible when column encryption is enabled.") : 0));
 	}
 
 	/*
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index a43f2e5553..e67462c81a 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -201,6 +203,12 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading user-defined collations");
 	(void) getCollations(fout, &numCollations);
 
+	pg_log_info("reading column master keys");
+	getColumnMasterKeys(fout);
+
+	pg_log_info("reading column encryption keys");
+	getColumnEncryptionKeys(fout);
+
 	pg_log_info("reading user-defined conversions");
 	getConversions(fout, &numConversions);
 
@@ -859,6 +867,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 9753a6d868..0a5178bcf1 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -484,6 +484,10 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('U', "USAGE");
 	}
+	else if (strcmp(type, "COLUMN ENCRYPTION KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "COLUMN MASTER KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "DATABASE") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,7 @@ typedef struct _connParams
 	char	   *pghost;
 	char	   *username;
 	trivalue	promptPassword;
+	int			column_encryption;
 	/* If not NULL, this overrides the dbname obtained from command line */
 	/* (but *only* the DB name, not anything else in the connstring) */
 	char	   *override_dbname;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 269bfce019..baf0de3347 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3429,6 +3429,8 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 
 	/* objects that don't require special decoration */
 	if (strcmp(type, "COLLATION") == 0 ||
+		strcmp(type, "COLUMN ENCRYPTION KEY") == 0 ||
+		strcmp(type, "COLUMN MASTER KEY") == 0 ||
 		strcmp(type, "CONVERSION") == 0 ||
 		strcmp(type, "DOMAIN") == 0 ||
 		strcmp(type, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 100644
--- a/src/bin/pg_dump/pg_backup_db.c
+++ b/src/bin/pg_dump/pg_backup_db.c
@@ -133,8 +133,8 @@ ConnectDatabase(Archive *AHX,
 	 */
 	do
 	{
-		const char *keywords[8];
-		const char *values[8];
+		const char *keywords[9];
+		const char *values[9];
 		int			i = 0;
 
 		/*
@@ -159,6 +159,11 @@ ConnectDatabase(Archive *AHX,
 		}
 		keywords[i] = "fallback_application_name";
 		values[i++] = progname;
+		if (cparams->column_encryption)
+		{
+			keywords[i] = "column_encryption";
+			values[i++] = "1";
+		}
 		keywords[i] = NULL;
 		values[i++] = NULL;
 		Assert(i <= lengthof(keywords));
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 1a06eeaf6a..2fd7e51938 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,8 @@ static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
 static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
 static void dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo);
 static void dumpCollation(Archive *fout, const CollInfo *collinfo);
+static void dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo);
+static void dumpColumnMasterKey(Archive *fout, const CmkInfo *cekinfo);
 static void dumpConversion(Archive *fout, const ConvInfo *convinfo);
 static void dumpRule(Archive *fout, const RuleInfo *rinfo);
 static void dumpAgg(Archive *fout, const AggInfo *agginfo);
@@ -393,6 +396,7 @@ main(int argc, char **argv)
 		{"attribute-inserts", no_argument, &dopt.column_inserts, 1},
 		{"binary-upgrade", no_argument, &dopt.binary_upgrade, 1},
 		{"column-inserts", no_argument, &dopt.column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &dopt.cparams.column_encryption, 1},
 		{"disable-dollar-quoting", no_argument, &dopt.disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &dopt.disable_triggers, 1},
 		{"enable-row-security", no_argument, &dopt.enable_row_security, 1},
@@ -685,6 +689,9 @@ main(int argc, char **argv)
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
 	 */
+	if (dopt.cparams.column_encryption && dopt.dump_inserts == 0)
+		pg_fatal("option --decrypt-encrypted-columns requires option --inserts, --rows-per-insert, or --column-inserts");
+
 	if (dopt.do_nothing && dopt.dump_inserts == 0)
 		pg_fatal("option --on-conflict-do-nothing requires option --inserts, --rows-per-insert, or --column-inserts");
 
@@ -1057,6 +1064,7 @@ help(const char *progname)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security (dump only content user has\n"
@@ -5572,6 +5580,164 @@ getCollations(Archive *fout, int *numCollations)
 	return collinfo;
 }
 
+/*
+ * getColumnEncryptionKeys
+ *	  get information about column encryption keys
+ */
+void
+getColumnEncryptionKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CekInfo	   *cekinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cekname;
+	int			i_ceknamespace;
+	int			i_cekowner;
+	int			i_cekacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, cek.cekowner, cek.cekacl, acldefault('Y', cek.cekowner) AS acldefault\n"
+					  "FROM pg_colenckey cek");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cekname = PQfnumber(res, "cekname");
+	i_ceknamespace = PQfnumber(res, "ceknamespace");
+	i_cekowner = PQfnumber(res, "cekowner");
+	i_cekacl = PQfnumber(res, "cekacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cekinfo = pg_malloc(ntups * sizeof(CekInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		PGresult	   *res2;
+		int				ntups2;
+
+		cekinfo[i].dobj.objType = DO_CEK;
+		cekinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cekinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cekinfo[i].dobj);
+		cekinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cekname));
+		cekinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cekacl));
+		cekinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cekinfo[i].dacl.privtype = 0;
+		cekinfo[i].dacl.initprivs = NULL;
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, ckdcmkalg, ckdencval\n"
+						  "FROM pg_catalog.pg_colenckeydata\n"
+						  "WHERE ckdcekid = %u", cekinfo[i].dobj.catId.oid);
+		res2 = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		ntups2 = PQntuples(res2);
+		cekinfo[i].numdata = ntups2;
+		cekinfo[i].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			cekinfo[i].cekcmkalgs[j] = atoi(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkalg")));
+			cekinfo[i].cekencvals[j] = pg_strdup(PQgetvalue(res2, j, PQfnumber(res2, "ckdencval")));
+		}
+		PQclear(res2);
+
+		selectDumpableObject(&(cekinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cekacl))
+			cekinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * getColumnMasterKeys
+ *	  get information about column master keys
+ */
+void
+getColumnMasterKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CmkInfo	   *cmkinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cmkname;
+	int			i_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+	int			i_cmkacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, cmk.cmkowner, cmk.cmkrealm, cmk.cmkacl, acldefault('y', cmk.cmkowner) AS acldefault\n"
+					  "FROM pg_colmasterkey cmk");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cmkname = PQfnumber(res, "cmkname");
+	i_cmknamespace = PQfnumber(res, "cmknamespace");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+	i_cmkacl = PQfnumber(res, "cmkacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cmkinfo = pg_malloc(ntups * sizeof(CmkInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		cmkinfo[i].dobj.objType = DO_CMK;
+		cmkinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cmkinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cmkinfo[i].dobj);
+		cmkinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cmkname));
+		cmkinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		cmkinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cmkacl));
+		cmkinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cmkinfo[i].dacl.privtype = 0;
+		cmkinfo[i].dacl.initprivs = NULL;
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cmkacl))
+			cmkinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8187,6 +8353,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8246,8 +8415,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
+	appendPQExpBuffer(q, "SELECT\n"
 						 "a.attrelid,\n"
 						 "a.attnum,\n"
 						 "a.attname,\n"
@@ -8260,7 +8428,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attlen,\n"
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
+						 "pg_catalog.format_type(%s) AS atttypname,\n"
 						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
@@ -8269,7 +8437,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "' ' || pg_catalog.quote_literal(option_value) "
 						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
 						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+						 "), E',\n    ') AS attfdwoptions,\n",
+						 fout->remoteVersion >= 160000 ?
+						 "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END, CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END" :
+						 "a.atttypid, a.atttypmod");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -8295,10 +8466,23 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	if (fout->remoteVersion >= 120000)
 		appendPQExpBufferStr(q,
-							 "a.attgenerated\n");
+							 "a.attgenerated,\n");
+	else
+		appendPQExpBufferStr(q,
+							 "'' AS attgenerated,\n");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBuffer(q,
+						  "a.attcek,\n"
+						  "CASE a.atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "CASE WHEN a.atttypid IN (%u, %u) THEN a.atttypmod END AS attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID,
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcek,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
@@ -8323,6 +8507,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	i_attencdet = PQfnumber(res, "attencdet");
+	i_attencalg = PQfnumber(res, "attencalg");
 	i_attisdropped = PQfnumber(res, "attisdropped");
 	i_attlen = PQfnumber(res, "attlen");
 	i_attalign = PQfnumber(res, "attalign");
@@ -8383,6 +8570,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		tbinfo->typstorage = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attidentity = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attgenerated = (char *) pg_malloc(numatts * sizeof(char));
+		tbinfo->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		tbinfo->attencdet = (bool *) pg_malloc(numatts * sizeof(bool));
+		tbinfo->attencalg = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char));
@@ -8410,6 +8600,22 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->attidentity[j] = *(PQgetvalue(res, r, i_attidentity));
 			tbinfo->attgenerated[j] = *(PQgetvalue(res, r, i_attgenerated));
 			tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
+			if (!PQgetisnull(res, r, i_attcek))
+			{
+				Oid		attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[j] = NULL;
+			if (!PQgetisnull(res, r, i_attencdet))
+				tbinfo->attencdet[j] = (PQgetvalue(res, r, i_attencdet)[0] == 't');
+			else
+				tbinfo->attencdet[j] = 0;
+			if (!PQgetisnull(res, r, i_attencalg))
+				tbinfo->attencalg[j] = atoi(PQgetvalue(res, r, i_attencalg));
+			else
+				tbinfo->attencalg[j] = 0;
 			tbinfo->attisdropped[j] = (PQgetvalue(res, r, i_attisdropped)[0] == 't');
 			tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
 			tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
@@ -9928,6 +10134,12 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_OPFAMILY:
 			dumpOpfamily(fout, (const OpfamilyInfo *) dobj);
 			break;
+		case DO_CEK:
+			dumpColumnEncryptionKey(fout, (const CekInfo *) dobj);
+			break;
+		case DO_CMK:
+			dumpColumnMasterKey(fout, (const CmkInfo *) dobj);
+			break;
 		case DO_COLLATION:
 			dumpCollation(fout, (const CollInfo *) dobj);
 			break;
@@ -13321,6 +13533,141 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * dumpColumnEncryptionKey
+ *	  dump the definition of the given column encryption key
+ */
+static void
+dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcekname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcekname = pg_strdup(fmtId(cekinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN ENCRYPTION KEY %s;\n",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_name(cekinfo->cekcmkalgs[i]));
+		appendPQExpBuffer(query, "encrypted_value = ");
+		appendStringLiteralAH(query, cekinfo->cekencvals[i], fout);
+
+		appendPQExpBuffer(query, ")");
+		if (i < cekinfo->numdata - 1)
+			appendPQExpBuffer(query, ", ");
+	}
+
+	appendPQExpBufferStr(query, ";\n");
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cekinfo->dobj.catId, cekinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cekinfo->dobj.name,
+								  .namespace = cekinfo->dobj.namespace->dobj.name,
+								  .owner = cekinfo->rolname,
+								  .description = "COLUMN ENCRYPTION KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cekinfo->dobj.dumpId, InvalidDumpId, "COLUMN ENCRYPTION KEY",
+				qcekname, NULL, cekinfo->dobj.namespace->dobj.name,
+				cekinfo->rolname, &cekinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcekname);
+}
+
+/*
+ * dumpColumnMasterKey
+ *	  dump the definition of the given column master key
+ */
+static void
+dumpColumnMasterKey(Archive *fout, const CmkInfo *cmkinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcmkname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcmkname = pg_strdup(fmtId(cmkinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN MASTER KEY %s;\n",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "realm = ");
+	appendStringLiteralAH(query, cmkinfo->cmkrealm, fout);
+
+	appendPQExpBufferStr(query, ");\n");
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cmkinfo->dobj.catId, cmkinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cmkinfo->dobj.name,
+								  .namespace = cmkinfo->dobj.namespace->dobj.name,
+								  .owner = cmkinfo->rolname,
+								  .description = "COLUMN MASTER KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN MASTER KEY", qcmkname,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cmkinfo->dobj.dumpId, InvalidDumpId, "COLUMN MASTER KEY",
+				qcmkname, NULL, cmkinfo->dobj.namespace->dobj.name,
+				cmkinfo->rolname, &cmkinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15404,6 +15751,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[j]));
+						/*
+						 * To reduce output size, we don't print the default
+						 * of encryption_type, but we do print the default of
+						 * algorithm, since we might want to change to a new
+						 * default algorithm sometime in the future.
+						 */
+						if (tbinfo->attencdet[j])
+							appendPQExpBuffer(q, "encryption_type = deterministic, ");
+						appendPQExpBuffer(q, "algorithm = '%s')",
+										  get_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -17967,6 +18330,8 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_ACCESS_METHOD:
 			case DO_OPCLASS:
 			case DO_OPFAMILY:
+			case DO_CEK:
+			case DO_CMK:
 			case DO_COLLATION:
 			case DO_CONVERSION:
 			case DO_TABLE:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index cdca0b993d..d4a2e595d0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -47,6 +47,8 @@ typedef enum
 	DO_ACCESS_METHOD,
 	DO_OPCLASS,
 	DO_OPFAMILY,
+	DO_CEK,
+	DO_CMK,
 	DO_COLLATION,
 	DO_CONVERSION,
 	DO_TABLE,
@@ -332,6 +334,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	struct _CekInfo **attcek;
+	int		   *attencalg;
+	bool	   *attencdet;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
 	char	   *attalign;		/* attribute align, used by binary_upgrade */
 	bool	   *attislocal;		/* true if attr has local definition */
@@ -663,6 +668,32 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	struct _CmkInfo **cekcmks;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -683,6 +714,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -710,6 +743,8 @@ extern AccessMethodInfo *getAccessMethods(Archive *fout, int *numAccessMethods);
 extern OpclassInfo *getOpclasses(Archive *fout, int *numOpclasses);
 extern OpfamilyInfo *getOpfamilies(Archive *fout, int *numOpfamilies);
 extern CollInfo *getCollations(Archive *fout, int *numCollations);
+extern void getColumnEncryptionKeys(Archive *fout);
+extern void getColumnMasterKeys(Archive *fout);
 extern ConvInfo *getConversions(Archive *fout, int *numConversions);
 extern TableInfo *getTables(Archive *fout, int *numTables);
 extern void getOwnedSeqs(Archive *fout, TableInfo tblinfo[], int numTables);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 8266c117a3..d3dacd39da 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -69,6 +69,8 @@ enum dbObjectTypePriorities
 	PRIO_TSTEMPLATE,
 	PRIO_TSDICT,
 	PRIO_TSCONFIG,
+	PRIO_CMK,
+	PRIO_CEK,
 	PRIO_FDW,
 	PRIO_FOREIGN_SERVER,
 	PRIO_TABLE,
@@ -111,6 +113,8 @@ static const int dbObjectTypePriority[] =
 	PRIO_ACCESS_METHOD,			/* DO_ACCESS_METHOD */
 	PRIO_OPFAMILY,				/* DO_OPCLASS */
 	PRIO_OPFAMILY,				/* DO_OPFAMILY */
+	PRIO_CEK,					/* DO_CEK */
+	PRIO_CMK,					/* DO_CMK */
 	PRIO_COLLATION,				/* DO_COLLATION */
 	PRIO_CONVERSION,			/* DO_CONVERSION */
 	PRIO_TABLE,					/* DO_TABLE */
@@ -1322,6 +1326,16 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "OPERATOR FAMILY %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_CEK:
+			snprintf(buf, bufsize,
+					 "COLUMN ENCRYPTION KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
+		case DO_CMK:
+			snprintf(buf, bufsize,
+					 "COLUMN MASTER KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_COLLATION:
 			snprintf(buf, bufsize,
 					 "COLLATION %s  (ID %d OID %u)",
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index cd421c5944..6530fb81a2 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -93,6 +93,7 @@ static bool dosync = true;
 
 static int	binary_upgrade = 0;
 static int	column_inserts = 0;
+static int	decrypt_encrypted_columns = 0;
 static int	disable_dollar_quoting = 0;
 static int	disable_triggers = 0;
 static int	if_exists = 0;
@@ -154,6 +155,7 @@ main(int argc, char *argv[])
 		{"attribute-inserts", no_argument, &column_inserts, 1},
 		{"binary-upgrade", no_argument, &binary_upgrade, 1},
 		{"column-inserts", no_argument, &column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &decrypt_encrypted_columns, 1},
 		{"disable-dollar-quoting", no_argument, &disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &disable_triggers, 1},
 		{"exclude-database", required_argument, NULL, 6},
@@ -424,6 +426,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --binary-upgrade");
 	if (column_inserts)
 		appendPQExpBufferStr(pgdumpopts, " --column-inserts");
+	if (decrypt_encrypted_columns)
+		appendPQExpBufferStr(pgdumpopts, " --decrypt-encrypted-columns");
 	if (disable_dollar_quoting)
 		appendPQExpBufferStr(pgdumpopts, " --disable-dollar-quoting");
 	if (disable_triggers)
@@ -649,6 +653,7 @@ help(void)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --exclude-database=PATTERN   exclude databases whose name matches PATTERN\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index d92247c915..f4f13fd087 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -645,6 +645,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
 	'ALTER FOREIGN DATA WRAPPER dummy OWNER TO' => {
 		regexp => qr/^ALTER FOREIGN DATA WRAPPER dummy OWNER TO .+;/m,
 		like   => { %full_runs, section_pre_data => 1, },
@@ -1245,6 +1257,26 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1663,6 +1695,26 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3496,6 +3548,26 @@
 		unlike => { no_privs => 1, },
 	},
 
+	'GRANT USAGE ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
+	'GRANT USAGE ON COLUMN MASTER KEY cmk1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
 	'GRANT USAGE ON FOREIGN DATA WRAPPER dummy' => {
 		create_order => 85,
 		create_sql   => 'GRANT USAGE ON FOREIGN DATA WRAPPER dummy
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..0d6a46d24b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c8a0bb7b3a..04d437f836 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1530,7 +1530,7 @@ describeOneTableDetails(const char *schemaname,
 	bool		printTableInitialized = false;
 	int			i;
 	char	   *view_def = NULL;
-	char	   *headers[12];
+	char	   *headers[13];
 	PQExpBufferData title;
 	PQExpBufferData tmpbuf;
 	int			cols;
@@ -1546,6 +1546,7 @@ describeOneTableDetails(const char *schemaname,
 				fdwopts_col = -1,
 				attstorage_col = -1,
 				attcompression_col = -1,
+				attcekname_col = -1,
 				attstattarget_col = -1,
 				attdescr_col = -1;
 	int			numrows;
@@ -1568,6 +1569,8 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attusertypid;
+	const char *attusertypmod;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1844,7 +1847,17 @@ describeOneTableDetails(const char *schemaname,
 	cols = 0;
 	printfPQExpBuffer(&buf, "SELECT a.attname");
 	attname_col = cols++;
-	appendPQExpBufferStr(&buf, ",\n  pg_catalog.format_type(a.atttypid, a.atttypmod)");
+	if (pset.sversion >= 160000)
+	{
+		attusertypid = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END";
+		attusertypmod = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END";
+	}
+	else
+	{
+		attusertypid = "a.atttypid";
+		attusertypmod = "a.atttypmod";
+	}
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, %s)", attusertypid, attusertypmod);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1857,7 +1870,8 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		attrdef_col = cols++;
 		attnotnull_col = cols++;
-		appendPQExpBufferStr(&buf, ",\n  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
+		appendPQExpBufferStr(&buf, ",\n"
+							 "  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
 							 "   WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) AS attcollation");
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
@@ -1909,6 +1923,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			appendPQExpBufferStr(&buf, ",\n  (SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek) AS attcekname");
+			attcekname_col = cols++;
+		}
+
 		/* stats target, if relevant to relkind */
 		if (tableinfo.relkind == RELKIND_RELATION ||
 			tableinfo.relkind == RELKIND_INDEX ||
@@ -2032,6 +2058,8 @@ describeOneTableDetails(const char *schemaname,
 		headers[cols++] = gettext_noop("Storage");
 	if (attcompression_col >= 0)
 		headers[cols++] = gettext_noop("Compression");
+	if (attcekname_col >= 0)
+		headers[cols++] = gettext_noop("Encryption");
 	if (attstattarget_col >= 0)
 		headers[cols++] = gettext_noop("Stats target");
 	if (attdescr_col >= 0)
@@ -2124,6 +2152,17 @@ describeOneTableDetails(const char *schemaname,
 							  false, false);
 		}
 
+		/* Column encryption */
+		if (attcekname_col >= 0)
+		{
+			if (!PQgetisnull(res, i, attcekname_col))
+				printTableAddCell(&cont, PQgetvalue(res, i, attcekname_col),
+								  false, false);
+			else
+				printTableAddCell(&cont, "",
+								  false, false);
+		}
+
 		/* Statistics target, if the relkind supports this feature */
 		if (attstattarget_col >= 0)
 			printTableAddCell(&cont, PQgetvalue(res, i, attstattarget_col),
@@ -4477,6 +4516,152 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cekacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cmkacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cmk.oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 554fe86725..1cf8f72176 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1729966959 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,8 @@ helpVariables(unsigned short int pager)
 		  "    true if last query failed, else false\n");
 	HELP0("  FETCH_COUNT\n"
 		  "    the number of result rows to fetch and display at a time (0 = unlimited)\n");
+	HELP0("  HIDE_COLUMN_ENCRYPTION\n"
+		  "    if set, column encryption details are not displayed\n");
 	HELP0("  HIDE_TABLEAM\n"
 		  "    if set, table access methods are not displayed\n");
 	HELP0("  HIDE_TOAST_COMPRESSION\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 73d4b393bc..010bc5a6d5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,7 @@ typedef struct _psqlSettings
 	bool		quiet;
 	bool		singleline;
 	bool		singlestep;
+	bool		hide_column_encryption;
 	bool		hide_compression;
 	bool		hide_tableam;
 	int			fetch_count;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 5a28b6f713..6736505c3a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -1188,6 +1188,13 @@ hide_compression_hook(const char *newval)
 							 &pset.hide_compression);
 }
 
+static bool
+hide_column_encryption_hook(const char *newval)
+{
+	return ParseVariableBool(newval, "HIDE_COLUMN_ENCRYPTION",
+							 &pset.hide_column_encryption);
+}
+
 static bool
 hide_tableam_hook(const char *newval)
 {
@@ -1259,6 +1266,9 @@ EstablishVariableSpace(void)
 	SetVariableHooks(pset.vars, "SHOW_CONTEXT",
 					 show_context_substitute_hook,
 					 show_context_hook);
+	SetVariableHooks(pset.vars, "HIDE_COLUMN_ENCRYPTION",
+					 bool_substitute_hook,
+					 hide_column_encryption_hook);
 	SetVariableHooks(pset.vars, "HIDE_TOAST_COMPRESSION",
 					 bool_substitute_hook,
 					 hide_compression_hook);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5e1882eaea..72add64773 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -933,6 +933,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1223,6 +1237,8 @@ static const pgsql_thing_t words_after_create[] = {
 	{"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", NULL, NULL, &Query_for_list_of_collations},
+	{"COLUMN ENCRYPTION KEY", NULL, NULL, NULL},
+	{"COLUMN MASTER KEY KEY", NULL, NULL, NULL},
 
 	/*
 	 * CREATE CONSTRAINT TRIGGER is not supported here because it is designed
@@ -1705,7 +1721,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1952,6 +1968,22 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "COLLATION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "REFRESH VERSION", "RENAME TO", "SET SCHEMA");
 
+	/* ALTER/DROP COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "ENCRYPTION", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_ceks);
+
+	/* ALTER COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("ADD VALUE (", "DROP VALUE (", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2894,6 +2926,26 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("true", "false");
 	}
 
+	/* CREATE/ALTER/DROP COLUMN ... KEY */
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN"))
+		COMPLETE_WITH("ENCRYPTION", "MASTER");
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN", "ENCRYPTION|MASTER"))
+		COMPLETE_WITH("KEY");
+
+	/* CREATE COLUMN ENCRYPTION KEY */
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("VALUES");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH", "VALUES"))
+		COMPLETE_WITH("(");
+
+	/* CREATE COLUMN MASTER KEY */
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("(");
+
 	/* CREATE DATABASE */
 	else if (Matches("CREATE", "DATABASE", MatchAny))
 		COMPLETE_WITH("OWNER", "TEMPLATE", "ENCODING", "TABLESPACE",
@@ -3619,6 +3671,7 @@ psql_completion(const char *text, int start, int end)
 			 Matches("DROP", "ACCESS", "METHOD", MatchAny) ||
 			 (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
 			  ends_with(prev_wd, ')')) ||
+			 Matches("DROP", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
 			 Matches("DROP", "EVENT", "TRIGGER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "TABLE", MatchAny) ||
@@ -3931,6 +3984,8 @@ psql_completion(const char *text, int start, int end)
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"COLUMN ENCRYPTION KEY",
+											"COLUMN MASTER KEY",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4046,6 +4101,16 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+	/* Complete "GRANT/REVOKE * ON COLUMN ENCRYPTION|MASTER KEY *" with TO/FROM */
+	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
+			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny))
+	{
+		if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny, MatchAny))
+			COMPLETE_WITH("TO");
+		else
+			COMPLETE_WITH("FROM");
+	}
+
 	/* Complete "GRANT/REVOKE * ON FOREIGN DATA WRAPPER *" with TO/FROM */
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny))
diff --git a/src/common/Makefile b/src/common/Makefile
index 113029bf7b..73dce1150e 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			return "AEAD_AES_128_CBC_HMAC_SHA_256";
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			return "AEAD_AES_192_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			return "AEAD_AES_256_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			return "AEAD_AES_256_CBC_HMAC_SHA_512";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index 41bd58ebdf..3695d3285b 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -4,6 +4,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 747ecb800d..d199033651 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,6 +20,8 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index ffd5e9dc82..eb59e73c0a 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -92,6 +92,9 @@ typedef enum ObjectClass
 	OCLASS_TYPE,				/* pg_type */
 	OCLASS_CAST,				/* pg_cast */
 	OCLASS_COLLATION,			/* pg_collation */
+	OCLASS_CEK,					/* pg_colenckey */
+	OCLASS_CEKDATA,				/* pg_colenckeydata */
+	OCLASS_CMK,					/* pg_colmasterkey */
 	OCLASS_CONSTRAINT,			/* pg_constraint */
 	OCLASS_CONVERSION,			/* pg_conversion */
 	OCLASS_DEFAULT,				/* pg_attrdef */
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index d01ab504b6..758696b539 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 3179be09d3..9e2c5256f7 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,9 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_colmasterkey.h',
+  'pg_colenckey.h',
+  'pg_colenckeydata.h',
 ]
 
 bki_data = [
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index f64a0ec26b..d0b9e8458d 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -115,6 +115,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c4d6adcd3e..c58b79e3a7 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5b950129de..0e9e85ebf3 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index b561e17781..7910175a6a 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,17 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	bool		attislocal BKI_DEFAULT(t);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey);
+
+	/*
+	 * User-visible type and typmod, currently used for encrypted columns.
+	 * These are only set to nondefault values if they are different from
+	 * atttypid and attypmod.
+	 */
+	Oid			attusertypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+	int32		attusertypmod BKI_DEFAULT(-1);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..c57fa18a27
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+/* ----------------
+ *		pg_colenckey definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckey
+ * ----------------
+ */
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+	Oid			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		cekacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_TOAST(pg_colenckey, 8263, 8264);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_nsp_index, 8242, ColumnEncKeyNameNspIndexId, on pg_colenckey using btree(cekname name_ops, ceknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkeydata.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+/* ----------------
+ *		pg_colenckeydata definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckeydata
+ * ----------------
+ */
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..d3bfd36279
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+	aclitem		cmkacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, on pg_colmasterkey using btree(cmkname name_ops, cmknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c867d99563..ff06d52fd0 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index b2cdea66c4..114279fa64 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 91587b99d0..c21052a3f7 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e2a7642a2b..0d6e23c9c9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6355,6 +6355,14 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -11935,4 +11943,37 @@
   proname => 'any_value_transfn', prorettype => 'anyelement',
   proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8245',
+  proname => 'pg_encrypted_det_eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'pg_encrypted_det_ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 92bcaf2c73..85e4b5554e 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,16 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 519e570c8c..3c7ab2a8fe 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -294,6 +294,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPCATEGORY_USER		'U'
 #define  TYPCATEGORY_BITSTRING	'V' /* er ... "varbit"? */
 #define  TYPCATEGORY_UNKNOWN	'X'
+#define  TYPCATEGORY_ENCRYPTED	'Y'
 #define  TYPCATEGORY_INTERNAL	'Z'
 
 #define  TYPALIGN_CHAR			'c' /* char alignment (i.e. unaligned) */
diff --git a/src/include/commands/colenccmds.h b/src/include/commands/colenccmds.h
new file mode 100644
index 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/colenccmds.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COLENCCMDS_H
+#define COLENCCMDS_H
+
+#include "catalog/objectaddress.h"
+#include "parser/parse_node.h"
+
+extern ObjectAddress CreateCEK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt);
+extern ObjectAddress CreateCMK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e7c2b91a58..11f88c59ce 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,4 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml;.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index ac6407e9f6..39a286e58a 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -164,6 +164,7 @@ typedef struct Port
 	 */
 	char	   *database_name;
 	char	   *user_name;
+	bool		column_encryption_enabled;
 	char	   *cmdline_options;
 	List	   *guc_options;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f7d7f10f7d..4e2256f2c5 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -88,8 +88,7 @@ typedef uint64 AclMode;			/* a bitmask of privilege bits */
 #define ACL_REFERENCES	(1<<5)
 #define ACL_TRIGGER		(1<<6)
 #define ACL_EXECUTE		(1<<7)	/* for functions */
-#define ACL_USAGE		(1<<8)	/* for languages, namespaces, FDWs, and
-								 * servers */
+#define ACL_USAGE		(1<<8)	/* for various object types */
 #define ACL_CREATE		(1<<9)	/* for namespaces and databases */
 #define ACL_CREATE_TEMP (1<<10) /* for databases */
 #define ACL_CONNECT		(1<<11) /* for databases */
@@ -722,6 +721,7 @@ typedef struct ColumnDef
 	char	   *colname;		/* name of column */
 	TypeName   *typeName;		/* type of column */
 	char	   *compression;	/* compression method for column */
+	List	   *encryption;		/* encryption info for column */
 	int			inhcount;		/* number of times column is inherited */
 	bool		is_local;		/* column has local (non-inherited) def'n */
 	bool		is_not_null;	/* NOT NULL constraint specified? */
@@ -758,11 +758,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1980,6 +1981,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2167,6 +2171,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..c8cf9c7776 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -149,6 +149,7 @@ PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("encryption", ENCRYPTION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -250,6 +251,7 @@ PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("master", MASTER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index d4865e50f6..26e1cd617a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..c1f6fe1410 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -29,6 +29,8 @@ PG_CMDTAG(CMDTAG_ALTER_ACCESS_METHOD, "ALTER ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_AGGREGATE, "ALTER AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CAST, "ALTER CAST", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_COLLATION, "ALTER COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY, "ALTER COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_MASTER_KEY, "ALTER COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONSTRAINT, "ALTER CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONVERSION, "ALTER CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_DATABASE, "ALTER DATABASE", false, false, false)
@@ -86,6 +88,8 @@ PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, fals
 PG_CMDTAG(CMDTAG_CREATE_AGGREGATE, "CREATE AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CAST, "CREATE CAST", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_COLLATION, "CREATE COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY, "CREATE COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_MASTER_KEY, "CREATE COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONSTRAINT, "CREATE CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONVERSION, "CREATE CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_DATABASE, "CREATE DATABASE", false, false, false)
@@ -138,6 +142,8 @@ PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CAST, "DROP CAST", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_COLLATION, "DROP COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_ENCRYPTION_KEY, "DROP COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_MASTER_KEY, "DROP COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONSTRAINT, "DROP CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONVERSION, "DROP CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_DATABASE, "DROP DATABASE", false, false, false)
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2..0c73022833 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -159,6 +159,8 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
+#define ACL_ALL_RIGHTS_CEK			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_CMK			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..1dac24e4b4 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -163,6 +163,7 @@ extern bool type_is_rowtype(Oid typid);
 extern bool type_is_enum(Oid typid);
 extern bool type_is_range(Oid typid);
 extern bool type_is_multirange(Oid typid);
+extern bool type_is_encrypted(Oid typid);
 extern void get_type_category_preferred(Oid typid,
 										char *typcategory,
 										bool *typispreferred);
@@ -202,6 +203,9 @@ extern Oid	get_publication_oid(const char *pubname, bool missing_ok);
 extern char *get_publication_name(Oid pubid, bool missing_ok);
 extern Oid	get_subscription_oid(const char *subname, bool missing_ok);
 extern char *get_subscription_name(Oid subid, bool missing_ok);
+extern char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok);
+extern char *get_cmk_name(Oid cmkid, bool missing_ok);
 
 #define type_is_array(typid)  (get_element_type(typid) != InvalidOid)
 /* type_is_array_domain accepts both plain arrays and domains over arrays */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index a443181d41..8ecacb29df 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d5d50ceab4..bf1d527c1a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAMENSP,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAMENSP,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c18e914228..10dfda8016 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,7 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 50b5df3490..bf6149e859 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -341,6 +341,14 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	{"cmklookup", "PGCMKLOOKUP", "", NULL,
+		"CMK-Lookup", "", 64,
+	offsetof(struct pg_conn, cmklookup)},
+
+	{"column_encryption", "PGCOLUMNENCRYPTION", "0", NULL,
+		"Column-Encryption", "", 1,
+	offsetof(struct pg_conn, column_encryption_setting)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -1412,6 +1420,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4029,6 +4059,22 @@ freePGconn(PGconn *conn)
 	free(conn->krbsrvname);
 	free(conn->gsslib);
 	free(conn->connip);
+	free(conn->cmklookup);
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		free(conn->cmks[i].cmkname);
+		free(conn->cmks[i].cmkrealm);
+	}
+	free(conn->cmks);
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		if (conn->ceks[i].cekdata)
+		{
+			explicit_bzero(conn->ceks[i].cekdata, conn->ceks[i].cekdatalen);
+			free(conn->ceks[i].cekdata);
+		}
+	}
+	free(conn->ceks);
 	/* Note that conn->Pfdebug is not ours to close or free */
 	free(conn->write_err_msg);
 	free(conn->inBuffer);
diff --git a/src/interfaces/libpq/fe-encrypt-openssl.c b/src/interfaces/libpq/fe-encrypt-openssl.c
new file mode 100644
index 0000000000..ab6c70967f
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,842 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt-openssl.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe-encrypt.h"
+#include "libpq-int.h"
+
+#include "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = NULL;
+	EVP_PKEY_CTX *ctx = NULL;
+	unsigned char *out = NULL;
+	size_t		outlen;
+
+	switch (cmkalg)
+	{
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			md = EVP_sha1();
+			break;
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		free(out);
+		out = NULL;
+		goto fail;
+	}
+
+	*tolen = outlen;
+
+fail:
+	EVP_PKEY_CTX_free(ctx);
+	EVP_PKEY_free(key);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05;
+ * for encrypting and decrypting column values.
+ */
+
+#ifdef TEST_ENCRYPT
+
+/*
+ * Test data from
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5;
+ */
+
+/*
+ * The different test cases just use different prefixes of K, so one constant
+ * is enough here.
+ */
+static const unsigned char K[] = {
+	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+	0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+	0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+	0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+};
+
+static const unsigned char P[] = {
+	0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20,
+	0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75,
+	0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65,
+	0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62,
+	0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69,
+	0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66,
+	0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f,
+	0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65,
+};
+
+static const unsigned char test_IV[] = {
+	0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04,
+};
+
+static const unsigned char test_A[] = {
+	0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63,
+	0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20,
+	0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+static const EVP_CIPHER *
+pg_cekalg_to_openssl_cipher(int cekalg)
+{
+	const EVP_CIPHER *cipher;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			cipher = EVP_aes_128_cbc();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			cipher = EVP_aes_192_cbc();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			cipher = EVP_aes_256_cbc();
+			break;
+		default:
+			cipher = NULL;
+	}
+
+	return cipher;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+static const EVP_MD *
+pg_cekalg_to_openssl_md(int cekalg)
+{
+	const EVP_MD *md;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			md = EVP_sha384();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			md = EVP_sha512();
+			break;
+		default:
+			md = NULL;
+	}
+
+	return md;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+static int
+md_key_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 16;
+	else if (md == EVP_sha384())
+		return 24;
+	else if (md == EVP_sha512())
+		return 32;
+	else
+		return -1;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+static int
+md_hash_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 32;
+	else if (md == EVP_sha384())
+		return 48;
+	else if (md == EVP_sha512())
+		return 64;
+	else
+		return -1;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+static bool
+get_message_auth_tag(const EVP_MD *md,
+					 const unsigned char *mac_key, int mac_key_len,
+					 const unsigned char *encr, int encrlen,
+					 unsigned char *md_value, size_t *md_len_p,
+					 const char **errmsgp)
+{
+	static char msgbuf[1024];
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	int64		al;
+	bool		result = false;
+
+	if (encrlen < 0)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("encrypted value has invalid length"));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, mac_key, mac_key_len);
+	if (!pkey)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("could not allocate key for HMAC: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, buf, bufsize))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, md_len_p))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	Assert(*md_len_p == md_hash_length(md));
+
+	/* truncate output to half the length, per spec */
+	*md_len_p /= 2;
+
+	result = true;
+fail:
+	free(buf);
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+
+/*
+ * Decrypt a column value
+ */
+unsigned char *
+decrypt_value(PGresult *res, const PGCEK *cek, int cekalg, const unsigned char *input, int inputlen, const char **errmsgp)
+{
+	static char msgbuf[1024];
+
+	const unsigned char *iv = NULL;
+	size_t		ivlen;
+
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *decr;
+	int			decrlen,
+				decrlen2;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized encryption algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized digest algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("column encryption key has wrong length for algorithm (has: %zu, required: %d)"),
+				 cek->cekdatalen, key_len);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_memcmp(input + (inputlen - md_len), md_value, md_len) != 0)
+	{
+		*errmsgp = libpq_gettext("MAC mismatch");
+		goto fail;
+	}
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	iv = input;
+	input += ivlen;
+	inputlen -= ivlen;
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = inputlen + EVP_CIPHER_CTX_block_size(evp_cipher_ctx) + 1;
+#ifndef TEST_ENCRYPT
+	buf = pqResultAlloc(res, bufsize, false);
+#else
+	buf = malloc(bufsize);
+#endif
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	decr = buf;
+	if (!EVP_DecryptUpdate(evp_cipher_ctx, decr, &decrlen, input, inputlen - md_len))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	if (!EVP_DecryptFinal_ex(evp_cipher_ctx, decr + decrlen, &decrlen2))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	decrlen += decrlen2;
+	Assert(decrlen < bufsize);
+	decr[decrlen] = '\0';
+	result = decr;
+
+fail:
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	return result;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#ifndef TEST_ENCRYPT
+static bool
+make_siv(PGconn *conn,
+		 unsigned char *iv, size_t ivlen,
+		 const EVP_MD *md,
+		 const unsigned char *iv_key, int iv_key_len,
+		 const unsigned char *plaintext, int plaintext_len)
+{
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	bool		result = false;
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, iv_key, iv_key_len);
+	if (!pkey)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	Assert(md_len == md_hash_length(md));
+	memcpy(iv, md_value, ivlen);
+
+	result = true;
+fail:
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+#endif							/* TEST_ENCRYPT */
+
+/*
+ * Encrypt a column value
+ */
+unsigned char *
+encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg, const unsigned char *value, int *nbytesp, bool enc_det)
+{
+	int			nbytes = *nbytesp;
+	unsigned char iv[EVP_MAX_IV_LENGTH];
+	size_t		ivlen;
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_key;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *encr;
+	int			encrlen,
+				encrlen2;
+
+	const char *errmsg;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		buf2size;
+	unsigned char *buf2 = NULL;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	Assert(ivlen <= sizeof(iv));
+	if (enc_det)
+	{
+#ifndef TEST_ENCRYPT
+		make_siv(conn, iv, ivlen, md, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	bufsize = ivlen + (nbytes + 2 * EVP_CIPHER_CTX_block_size(evp_cipher_ctx) - 1);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	encrlen += encrlen2;
+
+	encr -= ivlen;
+	encrlen += ivlen;
+
+	Assert(encrlen <= bufsize);
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  encr, encrlen,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf2, encr, encrlen);
+	memcpy(buf2 + encrlen, md_value, md_len);
+
+	result = buf2;
+	nbytes = buf2size;
+
+fail:
+	free(buf);
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	*nbytesp = nbytes;
+	return result;
+}
+
+
+/*
+ * Run test cases
+ */
+#ifdef TEST_ENCRYPT
+
+static void
+debug_print_hex(const char *name, const unsigned char *val, int len)
+{
+	printf("%s =", name);
+	for (int i = 0; i < len; i++)
+	{
+		if (i % 16 == 0)
+			printf("\n");
+		else
+			printf(" ");
+		printf("%02x", val[i]);
+	}
+	printf("\n");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, K_len);
+
+	C = encrypt_value(NULL, &cek, alg, P, &nbytes, true);
+	debug_print_hex("C", C, nbytes);
+}
+
+int
+main(int argc, char **argv)
+{
+	printf("5.1\n");
+	test_case(PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256, K, 32, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FE_ENCRYPT_H
+#define FE_ENCRYPT_H
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+extern unsigned char *decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+											int fromlen, const unsigned char *from,
+											int *tolen);
+
+extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg,
+									const unsigned char *input, int inputlen,
+									const char **errmsgp);
+
+extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg,
+									const unsigned char *value, int *nbytesp, bool enc_det);
+
+#endif							/* FE_ENCRYPT_H */
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index ec62550e38..7c7e2bac12 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,414 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+int
+pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+					  const char *keyrealm)
+{
+	char	   *keyname_copy;
+	char	   *keyrealm_copy;
+	bool		found;
+
+	keyname_copy = strdup(keyname);
+	if (!keyname_copy)
+		return EOF;
+	keyrealm_copy = strdup(keyrealm);
+	if (!keyrealm_copy)
+	{
+		free(keyname_copy);
+		return EOF;
+	}
+
+	found = false;
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		struct pg_cmk *checkcmk = &conn->cmks[i];
+
+		/* replace existing? */
+		if (checkcmk->cmkid == keyid)
+		{
+			free(checkcmk->cmkname);
+			free(checkcmk->cmkrealm);
+			checkcmk->cmkname = keyname_copy;
+			checkcmk->cmkrealm = keyrealm_copy;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newncmks;
+		struct pg_cmk *newcmks;
+		struct pg_cmk *newcmk;
+
+		newncmks = conn->ncmks + 1;
+		if (newncmks <= 0)
+			return EOF;
+		newcmks = realloc(conn->cmks, newncmks * sizeof(struct pg_cmk));
+		if (!newcmks)
+		{
+			free(keyname_copy);
+			free(keyrealm_copy);
+			return EOF;
+		}
+
+		newcmk = &newcmks[newncmks - 1];
+		newcmk->cmkid = keyid;
+		newcmk->cmkname = keyname_copy;
+		newcmk->cmkrealm = keyrealm_copy;
+
+		conn->ncmks = newncmks;
+		conn->cmks = newcmks;
+	}
+
+	return 0;
+}
+
+/*
+ * Replace placeholders in input string.  Return value malloc'ed.
+ */
+static char *
+replace_cmk_placeholders(const char *in, const char *cmkname, const char *cmkrealm, int cmkalg, const char *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					p++;
+					break;
+				case 'r':
+					appendPQExpBufferStr(&buf, cmkrealm);
+					p++;
+					break;
+				default:
+					appendPQExpBufferChar(&buf, p[0]);
+			}
+		}
+		else
+			appendPQExpBufferChar(&buf, p[0]);
+	}
+
+	return buf.data;
+}
+
+#ifndef USE_SSL
+/*
+ * Dummy implementation for non-SSL builds
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	return NULL;
+}
+#endif
+
+/*
+ * Decrypt a CEK using the given CMK.  The ciphertext is passed in
+ * "from" and "fromlen".  Return the decrypted value in a malloc'ed area, its
+ * length via "tolen".  Return NULL on error; add error messages directly to
+ * "conn".
+ */
+static unsigned char *
+decrypt_cek(PGconn *conn, const PGCMK *cmk, int cmkalg,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	char	   *cmklookup;
+	bool		found = false;
+	unsigned char *result = NULL;
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		return NULL;
+	}
+
+	/*
+	 * Analyze semicolon-separated list
+	 */
+	for (char *s = strtok(cmklookup, ";"); s; s = strtok(NULL, ";"))
+	{
+		char	   *sep;
+
+		/* split found token at '=' */
+		sep = strchr(s, '=');
+		if (!sep)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', s);
+			break;
+		}
+
+		/* matching realm? */
+		if (strncmp(s, "*", sep - s) == 0 || strncmp(s, cmk->cmkrealm, sep - s) == 0)
+		{
+			char	   *sep2;
+
+			found = true;
+
+			sep2 = strchr(sep, ':');
+			if (!sep2)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, "r");
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+int
+pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, const unsigned char *value, int len)
+{
+	PGCMK	   *cmk = NULL;
+	unsigned char *plainval = NULL;
+	int			plainvallen = 0;
+	bool		found;
+
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		if (conn->cmks[i].cmkid == cmkid)
+		{
+			cmk = &conn->cmks[i];
+			break;
+		}
+	}
+
+	if (!cmk)
+		return EOF;
+
+	plainval = decrypt_cek(conn, cmk, cmkalg, len, value, &plainvallen);
+	if (!plainval)
+		return EOF;
+
+	found = false;
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		struct pg_cek *checkcek = &conn->ceks[i];
+
+		/* replace existing? */
+		if (checkcek->cekid == keyid)
+		{
+			free(checkcek->cekdata);
+			checkcek->cekdata = plainval;
+			checkcek->cekdatalen = plainvallen;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newnceks;
+		struct pg_cek *newceks;
+		struct pg_cek *newcek;
+
+		newnceks = conn->nceks + 1;
+		if (newnceks <= 0)
+		{
+			free(plainval);
+			return EOF;
+		}
+		newceks = realloc(conn->ceks, newnceks * sizeof(struct pg_cek));
+		if (!newceks)
+		{
+			free(plainval);
+			return EOF;
+		}
+
+		newcek = &newceks[newnceks - 1];
+		newcek->cekid = keyid;
+		newcek->cekdata = plainval;
+		newcek->cekdatalen = plainvallen;
+
+		conn->nceks = newnceks;
+		conn->ceks = newceks;
+	}
+
+	return 0;
+}
 
 /*
  * pqRowProcessor
@@ -1251,13 +1662,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == res->attDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					*errmsgp = libpq_gettext("column encryption key not found");
+					goto fail;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				if (val == NULL)
+					goto fail;
+#else
+				*errmsgp = libpq_gettext("column encryption not supported by this build");
 				goto fail;
+#endif
+			}
+			else
+			{
+				val = (char *) pqResultAlloc(res, clen + 1, isbinary);
+				if (val == NULL)
+					goto fail;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* copy and zero-terminate the data (even if it's binary) */
+				memcpy(val, columns[i].value, clen);
+				val[clen] = '\0';
+			}
 
 			tup[i].len = clen;
 			tup[i].value = val;
@@ -1500,6 +1949,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1967,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2006,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2122,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2165,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2264,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2313,47 @@ PQsendQueryGuts(PGconn *conn,
 		goto sendFailed;
 
 	/* Send parameter formats */
-	if (nParams > 0 && paramFormats)
+	if (nParams > 0 && (paramFormats || (paramDesc && paramDesc->paramDescs)))
 	{
 		if (pqPutInt(nParams, 2, conn) < 0)
 			goto sendFailed;
+
 		for (i = 0; i < nParams; i++)
 		{
-			if (pqPutInt(paramFormats[i], 2, conn) < 0)
+			int			format = paramFormats ? paramFormats[i] : 0;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2372,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2390,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				bool		enc_det = (paramDesc->paramDescs[i].flags & 0x01) != 0;
+				PGCEK	   *cek = NULL;
+				char	   *enc_paramValue;
+				int			enc_nbytes = nbytes;
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == paramDesc->paramDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					libpq_append_conn_error(conn, "column encryption key not found");
+					goto sendFailed;
+				}
+
+				enc_paramValue = (char *) encrypt_value(conn, cek, paramDesc->paramDescs[i].cekalg,
+														(const unsigned char *) paramValue, &enc_nbytes, enc_det);
+				if (!enc_paramValue)
+					goto sendFailed;
+
+				if (pqPutInt(enc_nbytes, 4, conn) < 0 ||
+					pqPutnchar(enc_paramValue, enc_nbytes, conn) < 0)
+					goto sendFailed;
+
+				free(enc_paramValue);
+#else
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2872,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4140,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4188,17 @@ PQfmod(const PGresult *res, int field_num)
 		return 0;
 }
 
+int
+PQfisencrypted(const PGresult *res, int field_num)
+{
+	if (!check_field_number(res, field_num))
+		return false;
+	if (res->attDescs)
+		return (res->attDescs[field_num].cekid != 0);
+	else
+		return false;
+}
+
 char *
 PQcmdStatus(PGresult *res)
 {
@@ -3762,6 +4384,17 @@ PQparamtype(const PGresult *res, int param_num)
 		return InvalidOid;
 }
 
+int
+PQparamisencrypted(const PGresult *res, int param_num)
+{
+	if (!check_param_number(res, param_num))
+		return false;
+	if (res->paramDescs)
+		return (res->paramDescs[param_num].cekid != 0);
+	else
+		return false;
+}
+
 
 /* PQsetnonblocking:
  *	sets the PGconn's database connection non-blocking if the arg is true
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 8ab6a88416..3e3d8674be 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -43,6 +43,8 @@ static int	getRowDescriptions(PGconn *conn, int msgLength);
 static int	getParamDescriptions(PGconn *conn, int msgLength);
 static int	getAnotherTuple(PGconn *conn, int msgLength);
 static int	getParameterStatus(PGconn *conn);
+static int	getColumnMasterKey(PGconn *conn);
+static int	getColumnEncryptionKey(PGconn *conn);
 static int	getNotify(PGconn *conn);
 static int	getCopyStart(PGconn *conn, ExecStatusType copytype);
 static int	getReadyForQuery(PGconn *conn);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -547,6 +555,8 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		int			typlen;
 		int			atttypmod;
 		int			format;
+		int			cekid;
+		int			cekalg;
 
 		if (pqGets(&conn->workBuffer, conn) ||
 			pqGetInt(&tableid, 4, conn) ||
@@ -561,6 +571,21 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -582,8 +607,10 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		result->attDescs[i].typid = typid;
 		result->attDescs[i].typlen = typlen;
 		result->attDescs[i].atttypmod = atttypmod;
+		result->attDescs[i].cekid = cekid;
+		result->attDescs[i].cekalg = cekalg;
 
-		if (format != 1)
+		if ((format & 0x0F) != 1)
 			result->binary = 0;
 	}
 
@@ -685,10 +712,31 @@ getParamDescriptions(PGconn *conn, int msgLength)
 	for (i = 0; i < nparams; i++)
 	{
 		int			typid;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGetInt(&typid, 4, conn))
 			goto not_enough_data;
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&cekalg, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&flags, 2, conn))
+				goto not_enough_data;
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
 		result->paramDescs[i].typid = typid;
+		result->paramDescs[i].cekid = cekid;
+		result->paramDescs[i].cekalg = cekalg;
+		result->paramDescs[i].flags = flags;
 	}
 
 	/* Success! */
@@ -1468,6 +1516,92 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+/*
+ * Attempt to read a ColumnMasterKey message.
+ * Entry: 'y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	int			keyid;
+	char	   *keyname;
+	char	   *keyrealm;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	if (!keyname)
+		return EOF;
+	/* Get the key realm */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyrealm = strdup(conn->workBuffer.data);
+	if (!keyrealm)
+		return EOF;
+	/* And save it */
+	ret = pqSaveColumnMasterKey(conn, keyid, keyname, keyrealm);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(keyname);
+	free(keyrealm);
+
+	return ret;
+}
+
+/*
+ * Attempt to read a ColumnEncryptionKey message.
+ * Entry: 'Y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	int			keyid;
+	int			cmkid;
+	int			cmkalg;
+	char	   *buf;
+	int			vallen;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK ID */
+	if (pqGetInt(&cmkid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK algorithm */
+	if (pqGetInt(&cmkalg, 4, conn) != 0)
+		return EOF;
+	/* Get the key data len */
+	if (pqGetInt(&vallen, 4, conn) != 0)
+		return EOF;
+	/* Get the key data */
+	buf = malloc(vallen);
+	if (!buf)
+		return EOF;
+	if (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2420,9 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	if (conn->column_encryption_enabled)
+		ADD_STARTUP_OPTION("_pq_.column_encryption", "1");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index abaab6a073..77791e8349 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -450,7 +450,8 @@ pqTraceOutputS(FILE *f, const char *message, int *cursor)
 
 /* ParameterDescription */
 static void
-pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -458,12 +459,21 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 	nfields = pqTraceOutputInt16(f, message, cursor);
 
 	for (int i = 0; i < nfields; i++)
+	{
 		pqTraceOutputInt32(f, message, cursor, regress);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+			pqTraceOutputInt16(f, message, cursor);
+		}
+	}
 }
 
 /* RowDescription */
 static void
-pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -479,6 +489,11 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
 		pqTraceOutputInt16(f, message, cursor);
 		pqTraceOutputInt32(f, message, cursor, false);
 		pqTraceOutputInt16(f, message, cursor);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+		}
 	}
 }
 
@@ -514,6 +529,30 @@ pqTraceOutputW(FILE *f, const char *message, int *cursor, int length)
 		pqTraceOutputInt16(f, message, cursor);
 }
 
+/* ColumnMasterKey */
+static void
+pqTraceOutputy(FILE *f, const char *message, int *cursor, bool regress)
+{
+	fprintf(f, "ColumnMasterKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+}
+
+/* ColumnEncryptionKey */
+static void
+pqTraceOutputY(FILE *f, const char *message, int *cursor, bool regress)
+{
+	int			len;
+
+	fprintf(f, "ColumnEncryptionKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, false);
+	len = pqTraceOutputInt32(f, message, cursor, false);
+	pqTraceOutputNchar(f, len, message, cursor);
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -647,10 +686,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 				fprintf(conn->Pfdebug, "Sync"); /* no message content */
 			break;
 		case 't':				/* Parameter Description */
-			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'T':				/* Row Description */
-			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'v':				/* Negotiate Protocol Version */
 			pqTraceOutputv(conn->Pfdebug, message, &logCursor);
@@ -665,6 +706,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			fprintf(conn->Pfdebug, "Terminate");
 			/* No message content */
 			break;
+		case 'y':
+			pqTraceOutputy(conn->Pfdebug, message, &logCursor, regress);
+			break;
+		case 'Y':
+			pqTraceOutputY(conn->Pfdebug, message, &logCursor, regress);
+			break;
 		case 'Z':				/* Ready For Query */
 			pqTraceOutputZ(conn->Pfdebug, message, &logCursor);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f3d9220496..cf339a3e53 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -267,6 +267,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -438,6 +440,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,7 @@ extern int	PQfformat(const PGresult *res, int field_num);
 extern Oid	PQftype(const PGresult *res, int field_num);
 extern int	PQfsize(const PGresult *res, int field_num);
 extern int	PQfmod(const PGresult *res, int field_num);
+extern int	PQfisencrypted(const PGresult *res, int field_num);
 extern char *PQcmdStatus(PGresult *res);
 extern char *PQoidStatus(const PGresult *res);	/* old and ugly */
 extern Oid	PQoidValue(const PGresult *res);	/* new and improved */
@@ -540,6 +559,7 @@ extern int	PQgetlength(const PGresult *res, int tup_num, int field_num);
 extern int	PQgetisnull(const PGresult *res, int tup_num, int field_num);
 extern int	PQnparams(const PGresult *res);
 extern Oid	PQparamtype(const PGresult *res, int param_num);
+extern int	PQparamisencrypted(const PGresult *res, int param_num);
 
 /* Describe prepared statements and portals */
 extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d7ec5ed429..cbfaa7f95f 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -112,6 +112,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -343,6 +346,26 @@ typedef struct pg_conn_host
 								 * found in password file. */
 } pg_conn_host;
 
+/*
+ * Column encryption support data
+ */
+
+/* column master key */
+typedef struct pg_cmk
+{
+	Oid			cmkid;
+	char	   *cmkname;
+	char	   *cmkrealm;
+} PGCMK;
+
+/* column encryption key */
+typedef struct pg_cek
+{
+	Oid			cekid;
+	unsigned char *cekdata;		/* (decrypted) */
+	size_t		cekdatalen;
+} PGCEK;
+
 /*
  * PGconn stores all the state data associated with a single connection
  * to a backend.
@@ -396,6 +419,8 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *cmklookup;		/* CMK lookup specification */
+	char	   *column_encryption_setting;	/* column_encryption connection parameter (0 or 1) */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -477,6 +502,13 @@ struct pg_conn
 	PGVerbosity verbosity;		/* error/notice message verbosity */
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
+	bool		column_encryption_enabled;	/* parsed version of column_encryption_setting */
+
+	/* Column encryption support data */
+	int			ncmks;
+	PGCMK	   *cmks;
+	int			nceks;
+	PGCEK	   *ceks;
 
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
@@ -673,6 +705,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern int	pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+								  const char *keyrealm);
+extern int	pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg,
+									  const unsigned char *value, int len);
 extern int	pqRowProcessor(PGconn *conn, const char **errmsgp);
 extern void pqCommandQueueAdvance(PGconn *conn);
 extern int	PQsendQueryContinue(PGconn *conn, const char *query);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 573fd9b6ea..a6e6b2ada9 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,7 @@ endif
 
 if ssl.found()
   libpq_sources += files('fe-secure-common.c')
+  libpq_sources += files('fe-encrypt-openssl.c')
   libpq_sources += files('fe-secure-openssl.c')
 endif
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': get_option('ssl')},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 4df544ecef..0c36aa5f32 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
+GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-encrypt-openssl.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
 GETTEXT_TRIGGERS = libpq_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext pqInternalNotice:2
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 100644
--- a/src/interfaces/libpq/test/Makefile
+++ b/src/interfaces/libpq/test/Makefile
@@ -15,10 +15,17 @@ override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
 LDFLAGS_INTERNAL += $(libpq_pgport)
 
 PROGS = libpq_testclient libpq_uri_regress
+ifeq ($(with_ssl),openssl)
+PROGS += libpq_test_encrypt
+endif
+
 
 all: $(PROGS)
 
 $(PROGS): $(WIN32RES)
 
+libpq_test_encrypt: ../fe-encrypt-openssl.c
+	$(CC) $(CPPFLAGS) -DTEST_ENCRYPT $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 clean distclean maintainer-clean:
 	rm -f $(PROGS) *.o
diff --git a/src/interfaces/libpq/test/meson.build b/src/interfaces/libpq/test/meson.build
index b2a4b06fd2..87d2808b52 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c8ba170503 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -24,7 +24,7 @@ ifeq ($(with_ldap),yes)
 SUBDIRS += ldap
 endif
 ifeq ($(with_ssl),openssl)
-SUBDIRS += ssl
+SUBDIRS += column_encryption ssl
 endif
 
 # Test suites that are not safe by default but can be run if selected
@@ -36,7 +36,7 @@ export PG_TEST_EXTRA
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl column_encryption)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..d5ead874e5
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/column_encryption/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+export OPENSSL PERL
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/meson.build b/src/test/column_encryption/meson.build
new file mode 100644
index 0000000000..47f88c41ce
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,24 @@
+column_encryption_test_client = executable('test_client',
+  files('test_client.c'),
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+testprep_targets += column_encryption_test_client
+
+tests += {
+  'name': 'column_encryption',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_column_encryption.pl',
+      't/002_cmk_rotation.pl',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+      'PERL': perl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..c56af737ac
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,255 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+my $perl = $ENV{PERL};
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$openssl, 'pkeyutl', '-encrypt',
+		'-inkey', $cmkfilename,
+		'-pkeyopt', 'rsa_padding_mode:oaep',
+		'-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+		'-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+	# create CEK in database
+	$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = ${cmkname}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c smallint ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# Expected ciphertext length is 2 blocks of AES output (2 * 16) plus
+# half SHA-256 output (16) in hex encoding: (2 * 16 + 16) * 2 = 96
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1) TO STDOUT}),
+	qr/1\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text
+c|smallint),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result');
+
+{
+	local $ENV{PGCMKLOOKUP} = '*=run:broken %k %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = "*=run:$perl ./test_run_decrypt.pl %k %a %p";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$node->command_fails_like(['test_client', 'test1'], qr/not encrypted/, 'test client fails because parameters not encrypted');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result after test client insert');
+
+$node->command_ok(['test_client', 'test2'], 'test client test 2');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3|33),
+	'decrypted query result after test client insert 2');
+
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1 WHERE a = 3) TO STDOUT}),
+	qr/3\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3upd|33),
+	'decrypted query result after update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+is($node->safe_psql('postgres', q{SELECT b, count(*) FROM tbl2 GROUP BY b ORDER BY 2}),
+	q(valB|1
+valA|2),
+	'group by deterministically encrypted column');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl3 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c text ENCRYPTED WITH (column_encryption_key = cek2, algorithm = 'AEAD_AES_192_CBC_HMAC_SHA_384')
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1
+2|valB2|valC2
+3|valB3|valC3),
+	'decrypted query result multiple keys after second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..14eafb8ec9
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Test column master key rotation.  First, we generate CMK1 and a CEK
+# encrypted with it.  Then we add a CMK2 and encrypt the CEK with it
+# as well.  (Recall that a CEK can be associated with multiple CMKs,
+# for this reason.  That's why pg_colenckeydata is split out from
+# pg_colenckey.)  Then we remove CMK1.  We test that we can get
+# decrypted query results at each step.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# generate random bytes
+system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+# encrypt CEK using CMK
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk1filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# create CEK in database
+$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = cmk1, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b) VALUES (1, $1) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with one CMK');
+
+
+# create new CMK
+my $cmk2filename = create_cmk('cmk2');
+
+# encrypt CEK using new CMK
+#
+# (Here, we still have the plaintext of the CEK available from
+# earlier.  In reality, one would decrypt the CEK with the first CMK
+# and then re-encrypt it with the second CMK.)
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk2filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+$cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# add new data record for CEK in database
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} ADD VALUE (column_master_key = cmk2, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with two CMKs');
+
+
+# delete CEK record for first CMK
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} DROP VALUE (column_master_key = cmk1);});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with only new CMK');
+
+
+done_testing();
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (!(!PQparamisencrypted(res2, 0) &&
+		  PQparamisencrypted(res2, 1)))
+	{
+		fprintf(stderr, "wrong results from PQparamisencrypted()\n");
+		return 1;
+	}
+
+	res = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	PGconn	   *conn;
+	int			ret = 0;
+
+	conn = PQconnectdb("");
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		fprintf(stderr, "Connection to database failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (argc < 2 || argv[1] == NULL)
+		return 87;
+	else if (strcmp(argv[1], "test1") == 0)
+		ret = test1(conn);
+	else if (strcmp(argv[1], "test2") == 0)
+		ret = test2(conn);
+	else if (strcmp(argv[1], "test3") == 0)
+		ret = test3(conn);
+	else if (strcmp(argv[1], "test4") == 0)
+		ret = test4(conn);
+	else
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+
+# Test/sample command for libpq cmklookup run scheme
+#
+# This just places the data into temporary files and runs the openssl
+# command on it.  (In practice, this could more simply be written as a
+# shell script, but this way it's more portable.)
+
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $fh, '<:raw', "${tmpdir}/output.tmp" or die $!;
+my $data = '';
+
+while (1) {
+	my $success = read $fh, $data, 100, length($data);
+	die $! if not defined $success;
+	last if not $success;
+}
+
+close $fh;
+
+unlink "${tmpdir}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..d55ddb1ab7 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,7 @@ subdir('subscription')
 subdir('modules')
 
 if ssl.found()
+  subdir('column_encryption')
   subdir('ssl')
 endif
 
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..193ccacfff
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,353 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE ROLE regress_enc_user1;
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+CREATE COLUMN MASTER KEY cmk2;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "cek1" already has data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "fail" does not exist
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+ERROR:  column encryption key "notexist" does not exist
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+\d tbl_29f3
+              Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_29f3
+                                        Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+\d tbl_447f
+              Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_447f
+                                        Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  permission denied for column master key cmk1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+ERROR:  permission denied for column encryption key cek1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column master key cmk1 depends on column master key cmk1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ERROR:  column master key "cmk3" already exists in schema "public"
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+ERROR:  column master key "cmkx" does not exist
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ERROR:  column encryption key "cek3" already exists in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+ERROR:  must be owner of column encryption key cek3
+DROP COLUMN MASTER KEY cmk3;  -- fail
+ERROR:  must be owner of column master key cmk3
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ERROR:  column encryption key "cek1" has no data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ERROR:  column master key "fail" does not exist
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+ERROR:  attribute "algorithm" must not be specified
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+ERROR:  column encryption key "fail" does not exist
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf..77fedce8aa 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -107,7 +109,8 @@ BEGIN
         ('text search template'), ('text search configuration'),
         ('policy'), ('user mapping'), ('default acl'), ('transform'),
         ('operator of access method'), ('function of access method'),
-        ('publication namespace'), ('publication relation')
+        ('publication namespace'), ('publication relation'),
+        ('column encryption key'), ('column encryption key data'), ('column master key')
     LOOP
         FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
         LOOP
@@ -327,6 +330,24 @@ WARNING:  error for publication relation,{addr_nsp,zwei},{}: argument list lengt
 WARNING:  error for publication relation,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist
 WARNING:  error for publication relation,{eins,zwei,drei},{}: argument list length must be exactly 1
 WARNING:  error for publication relation,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei"
+WARNING:  error for column encryption key,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins},{}: column master key "eins" does not exist
+WARNING:  error for column master key,{eins},{integer}: column master key "eins" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 -- these object types cannot be qualified names
 SELECT pg_get_object_address('language', '{one}', '{}');
 ERROR:  language "one" does not exist
@@ -409,6 +430,9 @@ WITH objects (type, name, args) AS (VALUES
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,8 @@ drop cascades to user mapping for regress_addr_user on server integer
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 DROP SCHEMA addr_nsp CASCADE;
 NOTICE:  drop cascades to 14 other objects
 DETAIL:  drop cascades to text search dictionary addr_ts_dict
@@ -547,6 +576,9 @@ WITH objects (classid, objid, objsubid) AS (VALUES
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|NULL
+("(""column master key"",,,)")|("(""column master key"",,)")|NULL
+("(""column encryption key"",,,)")|("(""column encryption key"",,)")|NULL
+("(""column encryption key data"",,,)")|("(""column encryption key data"",,)")|NULL
 -- restore normal output mode
 \a\t
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..226f5e404e 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attusertypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,9 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,13 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
  anyrange                    | anymultirange
-(5 rows)
+(6 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -872,6 +876,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+pg_encrypted_det_eq(pg_encrypted_det,pg_encrypted_det)
+pg_encrypted_det_ne(pg_encrypted_det,pg_encrypted_det)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -75,7 +75,9 @@ ORDER BY t1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | pg_encrypted_det
+ 8244 | pg_encrypted_rnd
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT t1.oid, t1.typname as basetype, t2.typname as arraytype,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 15e015b3d6..577a6d4b2e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 427429975e..57c7d2bec3 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..6e63e6d4f5
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,265 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE ROLE regress_enc_user1;
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk2;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+\d tbl_29f3
+\d+ tbl_29f3
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+
+\d tbl_447f
+\d+ tbl_447f
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+
+
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..35af43032f 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -99,7 +101,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
         ('text search template'), ('text search configuration'),
         ('policy'), ('user mapping'), ('default acl'), ('transform'),
         ('operator of access method'), ('function of access method'),
-        ('publication namespace'), ('publication relation')
+        ('publication namespace'), ('publication relation'),
+        ('column encryption key'), ('column encryption key data'), ('column master key')
     LOOP
         FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
         LOOP
@@ -174,6 +177,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -228,6 +234,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 
 DROP SCHEMA addr_nsp CASCADE;
 
@@ -247,6 +255,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 2ddab010c2777c6a965cea82dc1b809ddc33ecc1
-- 
2.39.2



Attachments:

  [text/plain] v16-0001-Automatic-client-side-column-level-encryption.patch (434.8K, ../../[email protected]/2-v16-0001-Automatic-client-side-column-level-encryption.patch)
  download | inline diff:
From db66586f147abebba9d22ea416ae79032ebad5bb Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Wed, 22 Feb 2023 11:11:15 +0100
Subject: [PATCH v16] Automatic client-side column-level encryption

This feature enables the automatic encryption and decryption of
particular columns in the client.  The data for those columns then
only ever appears in ciphertext on the server, so it is protected from
DBAs, sysadmins, cloud operators, etc. as well as accidental leakage
to server logs, file-system backups, etc.  The canonical use case for
this feature is storing credit card numbers encrypted, in accordance
with PCI DSS, as well as similar situations involving social security
numbers etc.  One can't do any computations with encrypted values on
the server, but for these use cases, that is not necessary.  This
feature does support deterministic encryption as an alternative to the
default randomized encryption, so in that mode one can do equality
lookups, at the cost of some security.

This functionality also exists in other database products, and the
overall concepts were mostly adopted from there.

(Note: This feature has nothing to do with any on-disk encryption
feature.  Both can exist independently.)

You declare a column as encrypted in a CREATE TABLE statement.  The
column value is encrypted by a symmetric key called the column
encryption key (CEK).  The CEK is a catalog object.  The CEK key
material is in turn encrypted by an asymmetric key called the column
master key (CMK).  The CMK is not stored in the database but somewhere
where the client can get to it, for example in a file or in a key
management system.  When a server sends rows containing encrypted
column values to the client, it first sends the required CMK and CEK
information (new protocol messages), which the client needs to record.
Then, the client can use this information to automatically decrypt the
incoming row data and forward it in plaintext to the application.

For the CMKs, libpq has a new connection parameter "cmklookup" that
specifies via a mini-language where to get the keys.  Right now, you
can use "file" to read it from a file, or "run" to run some program,
which could get it from a KMS.

The general idea would be for an application to have one CMK per area
of secret stuff, for example, for credit card data.  The CMK can be
rotated: each CEK can be represented multiple times in the database,
encrypted by a different CMK.  (The CEK can't be rotated easily, since
that would require reading out all the data from a table/column and
reencrypting it.  We could/should add some custom tooling for that,
but it wouldn't be a routine operation.)

Several encryption algorithms are provided.  The CMK process uses
RSAES_OAEP_SHA_1 or _256.  The CEK process uses
AEAD_AES_*_CBC_HMAC_SHA_* with several strengths.

In the server, the encrypted datums are stored in types called
pg_encrypted_rnd and pg_encrypted_det (for randomized and
deterministic encryption).  These are essentially cousins of bytea.
For the rest of the database system below the protocol handling, there
is nothing special about those.  For example, pg_encrypted_rnd has no
operators at all, pg_encrypted_det has only an equality operator.
pg_attribute has a new column attrealtypid that stores the original
type of the data in the column.  This is only used for providing it to
clients, so that higher-level clients can convert the decrypted value
to their appropriate data types in their environments.

The protocol extensions are guarded by a new protocol extension option
"_pq_.column_encryption".  If this is not set, nothing changes, the
protocol stays the same, and no encryption or decryption happens.

To get automatically encrypted data into the database (as opposed to
reading it out), it is required to use protocol-level prepared
statements (i.e., extended query).  The client must first prepare a
statement, then describe the statement to get parameter metadata,
which indicates which parameters are to be encrypted and how.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this if the
\bind command is used.

Another challenge is that the parse analysis must check which
underlying column a parameter corresponds to.  This is similar to
resorigtbl and resorigcol in the opposite direction.

Discussion: https://www.postgresql.org/message-id/flat/89157929-c2b6-817b-6025-8e4b2d89d88f%40enterprisedb.com
---
 doc/src/sgml/acronyms.sgml                    |  18 +
 doc/src/sgml/catalogs.sgml                    | 317 +++++++
 doc/src/sgml/charset.sgml                     |  10 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 444 +++++++++
 doc/src/sgml/func.sgml                        |  26 +
 doc/src/sgml/glossary.sgml                    |  26 +
 doc/src/sgml/libpq.sgml                       | 322 +++++++
 doc/src/sgml/protocol.sgml                    | 441 +++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |  10 +
 .../ref/create_column_encryption_key.sgml     | 170 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  55 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/grant.sgml                   |  12 +-
 doc/src/sgml/ref/pg_dump.sgml                 |  42 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   7 +
 src/backend/access/common/printtup.c          | 222 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  60 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 288 ++++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  17 +
 src/backend/commands/colenccmds.c             | 439 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 204 ++++-
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 187 +++-
 src/backend/parser/parse_param.c              | 157 ++++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  53 ++
 src/backend/utils/adt/acl.c                   |  18 +
 src/backend/utils/adt/varlena.c               | 106 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  42 +
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/dumputils.c                   |   4 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 377 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  35 +
 src/bin/pg_dump/pg_dump_sort.c                |  14 +
 src/bin/pg_dump/pg_dumpall.c                  |   5 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  72 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 191 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  67 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |  11 +
 src/include/catalog/pg_colenckey.h            |  46 +
 src/include/catalog/pg_colenckeydata.h        |  46 +
 src/include/catalog/pg_colmasterkey.h         |  47 +
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  10 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               |  41 +
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  43 +-
 src/include/parser/kwlist.h                   |   2 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   6 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 842 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 665 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 139 ++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  24 +
 .../t/001_column_encryption.pl                | 255 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 353 ++++++++
 src/test/regress/expected/object_address.out  |  37 +-
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 265 ++++++
 src/test/regress/sql/object_address.sql       |  13 +-
 src/test/regress/sql/type_sanity.sql          |   2 +
 142 files changed, 9686 insertions(+), 85 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/alter_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_master_key.sgml
 create mode 100644 src/backend/commands/colenccmds.c
 create mode 100644 src/common/colenc.c
 create mode 100644 src/include/catalog/pg_colenckey.h
 create mode 100644 src/include/catalog/pg_colenckeydata.h
 create mode 100644 src/include/catalog/pg_colmasterkey.h
 create mode 100644 src/include/commands/colenccmds.h
 create mode 100644 src/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 create mode 100644 src/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/meson.build
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/t/002_cmk_rotation.pl
 create mode 100644 src/test/column_encryption/test_client.c
 create mode 100755 src/test/column_encryption/test_run_decrypt.pl
 create mode 100644 src/test/regress/expected/column_encryption.out
 create mode 100644 src/test/regress/sql/column_encryption.sql

diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 2df6559acc..3a5f2c254c 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -56,6 +56,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CEK</acronym></term>
+    <listitem>
+     <para>
+      Column Encryption Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CIDR</acronym></term>
     <listitem>
@@ -67,6 +76,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CMK</acronym></term>
+    <listitem>
+     <para>
+      Column Master Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CPAN</acronym></term>
     <listitem>
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1e4048054..808f29669d 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -105,6 +105,21 @@ <title>System Catalogs</title>
       <entry>collations (locale information)</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link></entry>
+      <entry>column encryption keys</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link></entry>
+      <entry>column encryption key data</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link></entry>
+      <entry>column master keys</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link></entry>
       <entry>check constraints, unique constraints, primary key constraints, foreign key constraints</entry>
@@ -1360,6 +1375,44 @@ <title><structname>pg_attribute</structname> Columns</title>
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attcek</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, a reference to the column encryption key, else 0.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type of the
+       encrypted data that is reported to the client.  If the column is not
+       encrypted, then 0.  For encrypted columns, the field
+       <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.
+       </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type
+       modifier (analogous to <structfield>atttypmod</structfield>) that is
+       reported to the client.  If the column is not encrypted, then -1.  For
+       encrypted columns, the field <structfield>atttypmod</structfield>)
+       contains the identifier of the encryption algorithm; see <xref
+       linkend="protocol-cek"/> for possible values.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>attinhcount</structfield> <type>int4</type>
@@ -2467,6 +2520,270 @@ <title><structname>pg_collation</structname> Columns</title>
   </para>
  </sect1>
 
+ <sect1 id="catalog-pg-colenckey">
+  <title><structname>pg_colenckey</structname></title>
+
+  <indexterm zone="catalog-pg-colenckey">
+   <primary>pg_colenckey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckey</structname> contains information
+   about the column encryption keys in the database.  The actual key material
+   of the column encryption keys is in the catalog <link
+   linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link>.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column encryption key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colenckeydata">
+  <title><structname>pg_colenckeydata</structname></title>
+
+  <indexterm zone="catalog-pg-colenckeydata">
+   <primary>pg_colenckeydata</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckeydata</structname> contains the key
+   material of column encryption keys.  Each column encryption key object can
+   contain several versions of the key material, each encrypted with a
+   different column master key.  That allows the gradual rotation of the
+   column master keys.  Thus, <literal>(ckdcekid, ckdcmkid)</literal> is a
+   unique key of this table.
+  </para>
+
+  <para>
+   The key material of column encryption keys should never be decrypted inside
+   the database instance.  It is meant to be sent as-is to the client, where
+   it is decrypted using the associated column master key, and then used to
+   encrypt or decrypt column values.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckeydata</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcekid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column encryption key this entry belongs to
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column master key that the key material is encrypted with
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkalg</structfield> <type>int4</type>
+      </para>
+      <para>
+       The encryption algorithm used for encrypting the key material; see
+       <xref linkend="protocol-cmk"/> for possible values.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdencval</structfield> <type>bytea</type>
+      </para>
+      <para>
+       The key material of this column encryption key, encrypted using the
+       referenced column master key
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colmasterkey">
+  <title><structname>pg_colmasterkey</structname></title>
+
+  <indexterm zone="catalog-pg-colmasterkey">
+   <primary>pg_colmasterkey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colmasterkey</structname> contains information
+   about column master keys.  The keys themselves are not stored in the
+   database.  The catalog entry only contains information that is used by
+   clients to locate the keys, for example in a file or in a key management
+   system.
+  </para>
+
+  <table>
+   <title><structname>pg_colmasterkey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column master key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkrealm</structfield> <type>text</type>
+      </para>
+      <para>
+       A <quote>realm</quote> associated with this column master key.  This is
+       a freely chosen string that is used by clients to determine how to look
+       up the key.  A typical configuration would put all CMKs that are looked
+       up in the same way into the same realm.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml
index 3032392b80..f3026fff83 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,16 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When automatic client-side column-level encryption is used, then no
+     encoding conversion is possible.  (The encoding conversion happens on the
+     server, and the server cannot look inside any encrypted column values.)
+     If automatic client-side column-level encryption is enabled for a
+     session, then the server enforces that the client encoding matches the
+     server encoding, and any attempts to change the client encoding will be
+     rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..67fce16872 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <title>Pseudo-Types</title>
 
   </sect1>
 
+  <sect1 id="datatype-encrypted">
+   <title>Types Related to Encryption</title>
+
+   <para>
+    An encrypted column value (see <xref linkend="ddl-column-encryption"/>) is
+    internally stored using the types
+    <type>pg_encrypted_rnd</type> (for randomized encryption) or
+    <type>pg_encrypted_det</type> (for deterministic encryption); see <xref
+    linkend="datatype-encrypted-table"/>.  Most of the database system treats
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support automatic client-side column-level encryption
+    or have disabled it will see the encrypted values in this format.  Clients
+    that support automatic client-side column-level encryption will not see
+    these types in result sets, as the protocol layer will translate them back
+    to the declared underlying type in the table definition.
+   </para>
+
+    <table id="datatype-encrypted-table">
+     <title>Types Related to Encryption</title>
+     <tgroup cols="3">
+      <colspec colname="col1" colwidth="1*"/>
+      <colspec colname="col2" colwidth="3*"/>
+      <colspec colname="col3" colwidth="2*"/>
+      <thead>
+       <row>
+        <entry>Name</entry>
+        <entry>Storage Size</entry>
+        <entry>Description</entry>
+       </row>
+      </thead>
+      <tbody>
+       <row>
+        <entry><type>pg_encrypted_det</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, deterministic encryption</entry>
+       </row>
+       <row>
+        <entry><type>pg_encrypted_rnd</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, randomized encryption</entry>
+       </row>
+      </tbody>
+     </tgroup>
+    </table>
+  </sect1>
  </chapter>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 8dc8d7a0ce..dfe73ca74c 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,440 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   With <firstterm>automatic client-side column-level encryption</firstterm>,
+   columns can be stored encrypted in the database.  The encryption and
+   decryption happens automatically on the client, so that the plaintext value
+   is never seen in the database instance or on the server hosting the
+   database.  The drawback is that most operations, such as function calls or
+   sorting, are not possible on encrypted values.
+  </para>
+
+  <sect2>
+   <title>Using Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   Automatic client-side column-level encryption uses two levels of
+   cryptographic keys.  The actual column value is encrypted using a symmetric
+   algorithm, such as AES, using a <firstterm>column encryption
+   key</firstterm> (<acronym>CEK</acronym>).  The column encryption key is in
+   turn encrypted using an asymmetric algorithm, such as RSA, using a
+   <firstterm>column master key</firstterm> (<acronym>CMK</acronym>).  The
+   encrypted CEK is stored in the database system.  The CMK is not stored in
+   the database system; it is stored on the client or somewhere where the
+   client can access it, such as in a local file or in a key management
+   system.  The database system only records where the CMK is stored and
+   provides this information to the client.  When rows containing encrypted
+   columns are sent to the client, the server first sends any necessary CMK
+   information, followed by any required CEK.  The client then looks up the
+   CMK and uses that to decrypt the CEK.  Then it decrypts incoming row data
+   using the CEK and provides the decrypted row data to the application.
+  </para>
+
+  <para>
+   Here is an example declaring a column as encrypted:
+<programlisting>
+CREATE TABLE customers (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    creditcard_num text <emphasis>ENCRYPTED WITH (column_encryption_key = cek1)</emphasis>
+);
+</programlisting>
+  </para>
+
+  <para>
+   Column encryption supports <firstterm>randomized</firstterm>
+   (also known as <firstterm>probabilistic</firstterm>) and
+   <firstterm>deterministic</firstterm> encryption.  The above example uses
+   randomized encryption, which is the default.  Randomized encryption uses a
+   random initialization vector for each encryption, so that even if the
+   plaintext of two rows is equal, the encrypted values will be different.
+   This prevents someone with direct access to the database server from making
+   computations such as distinct counts on the encrypted values.
+   Deterministic encryption uses a fixed initialization vector.  This reduces
+   security, but it allows equality searches on encrypted values.  The
+   following example declares a column with deterministic encryption:
+<programlisting>
+CREATE TABLE employees (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    ssn text ENCRYPTED WITH (
+        column_encryption_key = cek1, <emphasis>encryption_type = deterministic</emphasis>)
+);
+</programlisting>
+  </para>
+
+  <para>
+   Null values are not encrypted by automatic client-side column-level
+   encryption; null values sent by the client are visible as null values in
+   the database.  If the fact that a value is null needs to be hidden from the
+   server, this information needs to be encoded into a nonnull value in the
+   client somehow.
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Reading and Writing Encrypted Columns</title>
+
+   <para>
+    Reading and writing encrypted columns is meant to be handled automatically
+    by the client library/driver and should be mostly transparent to the
+    application code, if certain prerequisites are fulfilled:
+
+    <itemizedlist>
+     <listitem>
+      <para>
+       The client library needs to support automatic client-side column-level
+       encryption.  Not all client libraries do.  Furthermore, the client
+       library might require that automatic client-side column-level
+       encryption is explicitly enabled at connection time.  See the
+       documentation of the client library for details.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Column master keys and column encryption keys have been set up, and the
+       client library has been configured to be able to look up column master
+       keys from the key store or key management system.
+      </para>
+     </listitem>
+    </itemizedlist>
+   </para>
+
+   <para>
+    Reading from encrypted columns will then work automatically.  For example,
+    using the above example,
+<programlisting>
+SELECT ssn FROM employees WHERE id = 5;
+</programlisting>
+    would return the unencrypted value for the <literal>ssn</literal> column
+    in any rows found.
+   </para>
+
+   <para>
+    Writing to encrypted columns requires that the extended query protocol
+    (protocol-level prepared statements) be used, so that the values to be
+    encrypted are supplied separately from the SQL command.  For example,
+    using, say, psql or libpq, the following would not work:
+<programlisting>
+-- WRONG!
+INSERT INTO ssn (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This would leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of client-side column-level encryption.
+    (And even ignoring that, it could not work because the server does not
+    have access to the keys to perform the encryption.)  Note that using
+    server-side prepared statements using the SQL commands
+    <command>PREPARE</command> and <command>EXECUTE</command> is equally
+    incorrect, since that would also leak the parameters provided to
+    <command>EXECUTE</command> to the server.
+   </para>
+
+   <para>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</programlisting>
+    Higher-level client libraries might use the protocol-level prepared
+    statements automatically and thus won't require any code changes.
+   </para>
+
+   <para>
+    <application>psql</application> provides the command
+    <literal>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   The steps to set up automatic client-side column-level encryption for a
+   database are:
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK, for example, using a cryptographic
+      library or toolkit, or a key management system.  Secure access to the
+      key as appropriate, using access control, passwords, etc.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database using the SQL command <xref
+      linkend="sql-create-column-master-key"/>.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the (unencrypted) key material for the CEK in a temporary
+      location.  (It will be encrypted in the next step.  Depending on the
+      available tools, it might be possible and sensible to combine these two
+      steps.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material using the CMK (created earlier).
+      (The unencrypted version of the CEK key material can now be disposed
+      of.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database using the SQL command <xref
+      linkend="sql-create-column-encryption-key"/>.  This command
+      <quote>uploads</quote> the encrypted CEK key material created in the
+      previous step to the database server.  The local copy of the CEK key
+      material can then be removed.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns using the created CEK.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the client library/driver to be able to look up the CMK
+      created earlier.
+     </para>
+    </listitem>
+   </orderedlist>
+
+   Once this is done, values can be written to and read from the encrypted
+   columns in a transparent way.
+  </para>
+
+  <para>
+   Note that these steps should not be run on the database server, but on some
+   client machine.  Neither the CMK nor the unencrypted CEK should ever appear
+   on the database server host.
+  </para>
+
+  <para>
+   The specific details of this setup depend on the desired CMK storage
+   mechanism/key management system as well as the client libraries to be used.
+   The following example uses the <command>openssl</command> command-line tool
+   to set up the keys.
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK and write it to a file:
+<programlisting>
+openssl genpkey -algorithm rsa -out cmk1.pem
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database:
+<programlisting>
+psql ... -c "CREATE COLUMN MASTER KEY cmk1"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</programlisting>
+      (See <xref linkend="protocol-cek-table"/> for required key lengths.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material:
+<programlisting>
+openssl pkeyutl -encrypt -inkey cmk1.pem -pkeyopt rsa_padding_mode:oaep -in cek1.bin -out cek1.bin.enc
+rm cek1.bin
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database:
+<programlisting>
+# convert file contents to hex encoding; this is just one possible way
+cekenchex=$(perl -0ne 'print unpack "H*"' cek1.bin.enc)
+psql ... -c "CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, encrypted_value = '\\x${cekenchex}')"
+rm cek1.bin.enc
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns as shown in the examples above.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the libpq for CMK lookup (see also <xref linkend="libpq-connect-cmklookup"/>):
+<programlisting>
+PGCMKLOOKUP="*=file:$PWD/%k.pem"
+export PGCMKLOOKUP
+</programlisting>
+     </para>
+
+     <para>
+      Additionally, libpq requires that the connection parameter <xref
+      linkend="libpq-connect-column-encryption"/> be set in order to activate
+      the automatic client-side column-level encryption functionality.  This
+      should be done in the connection parameters of the application, but an
+      environment variable (<envar>PGCOLUMNENCRYPTION</envar>) is also
+      available.
+     </para>
+    </listitem>
+   </orderedlist>
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Guidance on Using Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use automatic client-side column-level encryption, and what precautions
+    need to be taken to maintain its security.
+   </para>
+
+   <para>
+    In general, column encryption is never a replacement for additional
+    security and encryption techniques such as transport encryption
+    (SSL/TLS), storage encryption, strong access control, and password
+    security.  Column encryption only targets specific use cases and should be
+    used in conjunction with additional security measures.
+   </para>
+
+   <para>
+    A typical use case for column encryption is to encrypt specific values
+    with additional security requirements, for example credit card numbers.
+    This allows you to store that security-sensitive data together with the
+    rest of your data (thus getting various benefits, such as referential
+    integrity, consistent backups), while giving access to that data only to
+    specific clients and preventing accidental leakage on the server side
+    (server logs, file system backups, etc.).
+   </para>
+
+   <para>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation; for
+    <application>libpq</application>, see <xref
+    linkend="libpq-connect-column-encryption"/>).
+   </para>
+
+   <para>
+    Column encryption cannot hide the existence or absence of data, it can
+    only disguise the particular data that is known to exist.  For example,
+    storing a cleartext person name and an encrypted credit card number
+    indicates that the person has a credit card.  That might not reveal too
+    much if the database is for an online store and there is other data nearby
+    that shows that the person has recently made purchases.  But in another
+    example, storing a cleartext person name and an encrypted diagnosis in a
+    medical database probably indicates that the person has a medical issue.
+    Depending on the circumstances, that might not by itself be sufficient
+    security.
+   </para>
+
+   <para>
+    Encryption cannot completely hide the length of values.  The encryption
+    methods will pad values to multiples of the underlying cipher's block size
+    (usually 16 bytes), so some length differences will be unified this way.
+    There is no concern if all values are of the same length, but if there are
+    signficant length differences between valid values and that length
+    information is security-sensitive, then application-specific workarounds
+    such as padding would need to be applied.  How to do that securely is
+    beyond the scope of this manual.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <tip>
+    <para>
+     One might be inclined to think of the client-side column-level encryption
+     feature as a mechanism for application writers and users to protect
+     themselves against an <quote>evil DBA</quote>, but that is not the
+     intended purpose.  Rather, it is (also) a tool for the DBA to control
+     which data they do not want (in plaintext) on the server.
+    </para>
+   </tip>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on is usually
+     subject to government or industry regulations.  This section is not meant
+     to provide complete instructions on how to do this correctly.  Please
+     seek additional advice when engaging in such projects.
+    </para>
+   </note>
+  </sect2>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
@@ -1985,6 +2419,14 @@ <title>Privileges</title>
        server.  Grantees may also create, alter, or drop their own user
        mappings associated with that server.
       </para>
+      <para>
+       For column master keys, allows the creation of column encryption keys
+       using the master key.
+      </para>
+      <para>
+       For column encryption keys, allows the use of the key in the creation
+       of table columns.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2151,6 +2593,8 @@ <title>ACL Privilege Abbreviations</title>
       <entry><literal>USAGE</literal></entry>
       <entry><literal>U</literal></entry>
       <entry>
+       <literal>COLUMN ENCRYPTION KEY</literal>,
+       <literal>COLUMN MASTER KEY</literal>,
        <literal>DOMAIN</literal>,
        <literal>FOREIGN DATA WRAPPER</literal>,
        <literal>FOREIGN SERVER</literal>,
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 0cbdf63632..137c0e366f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -23349,6 +23349,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 7c01a541fe..818038d860 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,32 @@ <title>Glossary</title>
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-column-encryption-key">
+   <glossterm>Column encryption key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt column values when using automatic
+     client-side column-level encryption (<xref
+     linkend="ddl-column-encryption"/>).  Column encryption keys are stored in
+     the database encrypted by another key, the <glossterm
+     linkend="glossary-column-master-key">column master key</glossterm>.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt <glossterm
+     linkend="glossary-column-encryption-key">column encryption
+     keys</glossterm>.  (So the column master key is a <firstterm>key
+     encryption key</firstterm>.)  Column master keys are stored outside the
+     database system, for example in a key management system.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-commit">
    <glossterm>Commit</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 0e7ae70c70..d9069e897f 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,141 @@ <title>Parameter Key Words</title>
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-connect-column-encryption" xreflabel="column_encryption">
+      <term><literal>column_encryption</literal></term>
+      <listitem>
+       <para>
+        If set to <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, this enables automatic client-side column-level
+        encryption for the connection.  If encrypted columns are queried and
+        this is not enabled, the encrypted values are returned.  See <xref
+        linkend="ddl-column-encryption"/> for more information about this
+        feature.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-cmklookup" xreflabel="cmklookup">
+      <term><literal>cmklookup</literal></term>
+      <listitem>
+       <para>
+        This specifies how libpq should look up column master keys (CMKs) in
+        order to decrypt the column encryption keys (CEKs).
+        The value is a list of <literal>key=value</literal> entries separated
+        by semicolons.  Each key is the name of a key realm, or
+        <literal>*</literal> to match all realms.  The value is a
+        <literal>scheme:data</literal> specification.  The scheme specifies
+        the method to look up the key, the remaining data is specific to the
+        scheme.  Placeholders are replaced in the remaining data as follows:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>%a</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name (see <xref linkend="protocol-cmk-table"/>)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%r</literal></term>
+          <listitem>
+           <para>
+            The realm name
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        Available schemes are:
+        <variablelist>
+         <varlistentry>
+          <term><literal>file</literal></term>
+          <listitem>
+           <para>
+            Load the key material from a file.  The remaining data is the file
+            name.  Use this if the CMKs are kept in a file on the file system.
+           </para>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>run</literal></term>
+          <listitem>
+           <para>
+            Run the specified command to decrypt the CEK.  The remaining data
+            is a shell command.  Use this with key management systems that
+            perform the decryption themselves.  The command must print the
+            decrypted plaintext on the standard output.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        The default value is empty.
+       </para>
+
+       <para>
+        Example:
+<programlisting>
+cmklookup="r1=file:/some/where/secrets/%k.pem;*=file:/else/where/%r/%k.pem"
+</programlisting>
+        This specification says, for keys in realm <quote>r1</quote>, load
+        them from the specified file, replacing <literal>%k</literal> by the
+        key name.  For keys in other realms, load them from the file,
+        replacing realm and key names as specified.
+       </para>
+
+       <para>
+        An example for interacting with a (hypothetical) key management
+        system:
+<programlisting>
+cmklookup="*=run:acmekms decrypt --key %k --alg %a --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2999,32 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            half-byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this half-byte to one to force
+            encryption.  For example, use the C code literal
+            <literal>0x10</literal> to specify text format with forced
+            encryption.  If the array pointer is null then encryption is not
+            forced for any parameter.
+           </para>
+
+           <para>
+            Parameters corresponding to encrypted columns must be passed in
+            text format.  Specifying binary format for such a parameter will
+            result in an error.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter does not
+            correspond to an encrypted column on the server, then the call
+            will fail and the parameter will not be sent.  This can be used
+            for additional security against a compromised server.  (The
+            drawback is that application code then needs to be kept up to date
+            with knowledge about which columns are encrypted rather than
+            letting the server specify this.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3037,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3196,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Sends a request to execute a prepared statement with given
+        parameters, and waits for the result, with support for encrypted columns.
+<synopsis>
+PGresult *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> is like <xref
+        linkend="libpq-PQexecPrepared"/> with additional support for encrypted
+        columns.  The parameter <parameter>paramDesc</parameter> must be a
+        result set obtained from <xref linkend="libpq-PQdescribePrepared"/> on
+        the same prepared statement.
+       </para>
+
+       <para>
+        This function must be used if a statement parameter corresponds to an
+        underlying encrypted column.  In that situation, the prepared
+        statement needs to be described first so that libpq can obtain the
+        necessary key and other information from the server.  When that is
+        done, the parameters corresponding to encrypted columns are
+        automatically encrypted appropriately before being sent to the server.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4084,28 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQfisencrypted">
+     <term><function>PQfisencrypted</function><indexterm><primary>PQfisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given column came from an encrypted
+       column.  Column numbers start at 0.
+<synopsis>
+int PQfisencrypted(const PGresult *res,
+                   int column_number);
+</synopsis>
+      </para>
+
+      <para>
+       Encrypted column values are automatically decrypted, so this function
+       is not necessary to access the column value.  It can be used for extra
+       security to check whether the value was stored encrypted when one
+       thought it should be.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQfsize">
      <term><function>PQfsize</function><indexterm><primary>PQfsize</primary></indexterm></term>
 
@@ -4059,6 +4287,31 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQparamisencrypted">
+     <term><function>PQparamisencrypted</function><indexterm><primary>PQparamisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given parameter is destined for an
+       encrypted column.  Parameter numbers start at 0.
+<synopsis>
+int PQparamisencrypted(const PGresult *res, int param_number);
+</synopsis>
+      </para>
+
+      <para>
+       Values for parameters destined for encrypted columns are automatically
+       encrypted, so this function is not necessary to prepare the parameter
+       value.  It can be used for extra security to check whether the value
+       will be stored encrypted when one thought it should be.  (But see also
+       at <xref linkend="libpq-PQexecPreparedDescribed"/> for another way to do that.)
+       This function is only useful when inspecting the result of <xref
+       linkend="libpq-PQdescribePrepared"/>.  For other types of results it
+       will return false.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQprint">
      <term><function>PQprint</function><indexterm><primary>PQprint</primary></indexterm></term>
 
@@ -4584,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4845,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4902,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4963,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Sends a request to execute a prepared statement with given
+       parameters, without waiting for the result(s), with support for encrypted columns.
+<synopsis>
+int PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> is like <xref
+       linkend="libpq-PQsendQueryPrepared"/> with additional support for encrypted
+       columns.  The parameter <parameter>paramDesc</parameter> must be a
+       result set obtained from <xref linkend="libpq-PQsendDescribePrepared"/> on
+       the same prepared statement.
+      </para>
+
+      <para>
+       This function must be used if a statement parameter corresponds to an
+       underlying encrypted column.  In that situation, the prepared
+       statement needs to be described first so that libpq can obtain the
+       necessary key and other information from the server.  When that is
+       done, the parameters corresponding to encrypted columns are
+       automatically encrypted appropriately before being sent to the server.
+       See also under <xref linkend="libpq-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5052,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8086,26 @@ <title>Environment Variables</title>
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCMKLOOKUP</envar></primary>
+      </indexterm>
+      <envar>PGCMKLOOKUP</envar> behaves the same as the <xref
+      linkend="libpq-connect-cmklookup"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCOLUMNENCRYPTION</envar></primary>
+      </indexterm>
+      <envar>PGCOLUMNENCRYPTION</envar> behaves the same as the <xref
+      linkend="libpq-connect-column-encryption"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 93fc7167d4..52c29bf8da 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,76 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-column-encryption">
+   <title>Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    Automatic client-side column-level encryption is enabled by sending the
+    parameter <literal>_pq_.column_encryption</literal> with a value of
+    <literal>1</literal> in the StartupMessage.  This is a protocol extension
+    that enables a few additional protocol messages and adds additional fields
+    to existing protocol messages.  Client drivers should only activate this
+    protocol extension when requested by the user, for example through a
+    connection parameter.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the
+    messages ColumnMasterKey and ColumnEncryptionKey can appear before
+    RowDescription and ParameterDescription messages.  Clients should collect
+    the information in these messages and keep them for the duration of the
+    connection.  A server is not required to resend the key information for
+    each statement cycle if it was already sent during this connection.  If a
+    server resends a key that the client has already stored (that is, a key
+    having an ID equal to one already stored), the new information should
+    replace the old.  (This could happen, for example, if the key was altered
+    by server-side DDL commands.)
+   </para>
+
+   <para>
+    A client supporting automatic column-level encryption should automatically
+    decrypt the column value fields of DataRow messages corresponding to
+    encrypted columns, and it should automatically encrypt the parameter value
+    fields of Bind messages corresponding to encrypted columns.
+   </para>
+
+   <para>
+    When column encryption is used, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column-level encryption protocol option is enabled.  That way, a client
+    library only needs to support decrypting data sent in binary and does not
+    have to support decoding the text format of the encryption-related types
+    (see <xref linkend="datatype-encrypted"/>).
+   </para>
+
+   <para>
+    When deterministic encryption is used, clients need to take care to
+    represent plaintext to be encrypted in a consistent form.  For example,
+    encrypting an integer represented by the string <literal>100</literal> and
+    an integer represented by the string <literal>+100</literal> would result
+    in two different ciphertexts, thus defeating the main point of
+    deterministic encryption.  This protocol specification requires the
+    plaintext to be in <quote>canonical</quote> form, which is the form that
+    is produced by the server when it outputs a particular value in text
+    format.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the client
+    encoding must match the server encoding.  This ensures that all values
+    encrypted or decrypted by the client match the server encoding.
+   </para>
+
+   <para>
+    The cryptographic operations used for automatic client-side column-level
+    encryption are described in <xref
+    linkend="protocol-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -4061,6 +4131,140 @@ <title>Message Formats</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ColumnEncryptionKey">
+    <term>ColumnEncryptionKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('Y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column encryption key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the master key used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the algorithm used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The length of the following key material.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Byte<replaceable>n</replaceable></term>
+       <listitem>
+        <para>
+         The key material, encrypted with the master key referenced above.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="protocol-message-formats-ColumnMasterKey">
+    <term>ColumnMasterKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column master key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The key's realm.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="protocol-message-formats-CommandComplete">
     <term>CommandComplete (B)</term>
     <listitem>
@@ -5157,6 +5361,45 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each parameter:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         This is used as a bit field of flags.  If the column is
+         encrypted and bit 0x01 is set, the column uses deterministic
+         encryption, otherwise randomized encryption.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5545,6 +5788,34 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each field:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7370,6 +7641,176 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-column-encryption-crypto">
+  <title>Automatic Client-side Column-level Encryption Cryptography</title>
+
+  <para>
+   This section describes the cryptographic operations used by the automatic
+   client-side column-level encryption functionality.  A client that supports
+   this functionality needs to implement these operations as specified here in
+   order to be able to interoperate with other clients.
+  </para>
+
+  <para>
+   Column encryption key algorithms and column master key algorithms are
+   identified by integers in the protocol messages and the system catalogs.
+   Additional algorithms may be added to this protocol specification without a
+   change in the protocol version number.  Clients should implement support
+   for all the algorithms specified here.  If a client encounters an algorithm
+   identifier it does not recognize or does not support, it must raise an
+   error.  A suitable error message should be provided to the application or
+   user.
+  </para>
+
+  <sect2 id="protocol-cmk">
+   <title>Column Master Keys</title>
+
+   <para>
+    The currently defined algorithms for column master keys are listed in
+    <xref linkend="protocol-cmk-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC
+       8017</ulink>/PKCS #1)</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect2>
+
+  <sect2 id="protocol-cek">
+   <title>Column Encryption Keys</title>
+
+   <para>
+    The currently defined algorithms for column encryption keys are listed in
+    <xref linkend="protocol-cek-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</entry>
+       <entry><literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</entry>
+       <entry><literal>AEAD_AES_192_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_512</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <para>
+    The <quote>associated data</quote> in these algorithms consists of 4
+    bytes: The ASCII letters <literal>P</literal> and <literal>G</literal>
+    (byte values 80 and 71), followed by the version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </para>
+
+   <para>
+    The length of the initialization vector is 16 octets for all CEK algorithm
+    variants.  For randomized encryption, the initialization vector should be
+    (cryptographically strong) random bytes.  For deterministic encryption,
+    the initialization vector is constructed as
+<programlisting>
+SUBSTRING(<replaceable>HMAC</replaceable>(<replaceable>K</replaceable>, <replaceable>P</replaceable>) FOR <replaceable>IVLEN</replaceable>)
+</programlisting>
+    where <replaceable>HMAC</replaceable> is the HMAC function associated with
+    the algorithm, <replaceable>K</replaceable> is the IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </para>
+  </sect2>
+ </sect1>
+
  <sect1 id="protocol-changes">
   <title>Summary of Changes since Protocol 2.0</title>
 
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 54b5f22d6e..a730e5d650 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -8,6 +8,8 @@
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
+<!ENTITY alterColumnEncryptionKey SYSTEM "alter_column_encryption_key.sgml">
+<!ENTITY alterColumnMasterKey SYSTEM "alter_column_master_key.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
 <!ENTITY alterDefaultPrivileges SYSTEM "alter_default_privileges.sgml">
@@ -62,6 +64,8 @@
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
+<!ENTITY createColumnEncryptionKey SYSTEM "create_column_encryption_key.sgml">
+<!ENTITY createColumnMasterKey SYSTEM "create_column_master_key.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
 <!ENTITY createDatabase     SYSTEM "create_database.sgml">
 <!ENTITY createDomain       SYSTEM "create_domain.sgml">
@@ -109,6 +113,8 @@
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
+<!ENTITY dropColumnEncryptionKey SYSTEM "drop_column_encryption_key.sgml">
+<!ENTITY dropColumnMasterKey SYSTEM "drop_column_master_key.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
 <!ENTITY dropDatabase       SYSTEM "drop_database.sgml">
 <!ENTITY dropDomain         SYSTEM "drop_domain.sgml">
diff --git a/doc/src/sgml/ref/alter_column_encryption_key.sgml b/doc/src/sgml/ref/alter_column_encryption_key.sgml
new file mode 100644
index 0000000000..655e1e00d8
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+doc/src/sgml/ref/alter_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-encryption-key">
+ <indexterm zone="sql-alter-column-encryption-key">
+  <primary>ALTER COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>change the definition of a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> ADD VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> DROP VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN ENCRYPTION KEY</command> changes the definition of a
+   column encryption key.
+  </para>
+
+  <para>
+   The first form adds new encrypted key data to a column encryption key,
+   which must be encrypted with a different column master key than the
+   existing key data.  The second form removes a key data entry for a given
+   column master key.  Together, these forms can be used for column master key
+   rotation.
+  </para>
+
+  <para>
+   You must own the column encryption key to use <command>ALTER COLUMN
+   ENCRYPTION KEY</command>.  To alter the owner, you must also be a direct or
+   indirect member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column encryption key's
+   schema.  (These restrictions enforce that altering the owner doesn't do
+   anything you couldn't do by dropping and recreating the column encryption
+   key.  However, a superuser can alter ownership of any column encryption key
+   anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column encryption
+      key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  See <xref
+      linkend="sql-create-column-encryption-key"/> for details
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rotate the master keys used to encrypt a given column encryption key,
+   use a command sequence like this:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (
+    COLUMN_MASTER_KEY = cmk1
+);
+</programlisting>
+  </para>
+
+  <para>
+   To rename the column encryption key <literal>cek1</literal> to
+   <literal>cek2</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column encryption key <literal>cek1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN ENCRYPTION KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/alter_column_master_key.sgml b/doc/src/sgml/ref/alter_column_master_key.sgml
new file mode 100644
index 0000000000..7f0e656ef0
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+doc/src/sgml/ref/alter_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-master-key">
+ <indexterm zone="sql-alter-column-master-key">
+  <primary>ALTER COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN MASTER KEY</refname>
+  <refpurpose>change the definition of a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> ( REALM = <replaceable>realm</replaceable> )
+
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </para>
+
+  <para>
+   You must own the column master key to use <command>ALTER COLUMN MASTER
+   KEY</command>.  To alter the owner, you must also be a direct or indirect
+   member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column master key's schema.
+   (These restrictions enforce that altering the owner doesn't do anything you
+   couldn't do by dropping and recreating the column master key.  However, a
+   superuser can alter ownership of any column master key anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the column master key <literal>cmk1</literal> to
+   <literal>cmk2</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column master key <literal>cmk1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN MASTER KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..1caf9bfa56 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..ebcbf5d00a 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,16 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support automatic client-side
+    column-level encryption or decryption; its input or output data will
+    always be the ciphertext.  This is usually suitable for backups (see also
+    <xref linkend="app-pgdump"/>).  If automatic client-side encryption or
+    decryption is wanted, <command>INSERT</command> and
+    <command>SELECT</command> need to be used instead to write and read the
+    data.
+   </para>
+
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/ref/create_column_encryption_key.sgml b/doc/src/sgml/ref/create_column_encryption_key.sgml
new file mode 100644
index 0000000000..56fb253dad
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,170 @@
+<!--
+doc/src/sgml/ref/create_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-encryption-key">
+ <indexterm zone="sql-create-column-encryption-key">
+  <primary>CREATE COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>define a new column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN ENCRYPTION KEY <replaceable>name</replaceable> WITH VALUES (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+[ , ... ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN ENCRYPTION KEY</command> defines a new column
+   encryption key.  A column encryption key is used for client-side encryption
+   of table columns that have been defined as encrypted.  The key material of
+   a column encryption key is stored in the database's system catalogs,
+   encrypted (wrapped) by a column master key (which in turn is only
+   accessible to the client, not the database server).
+  </para>
+
+  <para>
+   A column encryption key can be associated with more than one column master
+   key.  To specify that, specify more than one parenthesized definition (see
+   also the examples).
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column encryption key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.  You must have <literal>USAGE</literal> privilege on the
+      column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  Supported algorithms are:
+      <itemizedlist>
+       <listitem>
+        <para><literal>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></para>
+       </listitem>
+      </itemizedlist>
+     </para>
+
+     <para>
+      This is informational only.  The specified value is provided to the
+      client, which may use it for decrypting the column encryption key on the
+      client side.  But a client is also free to ignore this information and
+      figure out how to arrange the decryption in some other way.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+</programlisting>
+  </para>
+
+  <para>
+   To specify more than one associated column master key:
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\xF1F2F2F4...'
+);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_column_master_key.sgml b/doc/src/sgml/ref/create_column_master_key.sgml
new file mode 100644
index 0000000000..6aaa1088d1
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+doc/src/sgml/ref/create_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-master-key">
+ <indexterm zone="sql-create-column-master-key">
+  <primary>CREATE COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN MASTER KEY</refname>
+  <refpurpose>define a new column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN MASTER KEY <replaceable>name</replaceable> [ WITH (
+    [ REALM = <replaceable>realm</replaceable> ]
+) ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN MASTER KEY</command> defines a new column master
+   key.  A column master key is used to encrypt column encryption keys, which
+   are the keys that actually encrypt the column data.  The key material of
+   the column master key is not stored in the database.  The definition of a
+   column master key records information that will allow a client to locate
+   the key material, for example in a file or in a key management system.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column master key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>realm</replaceable></term>
+
+    <listitem>
+     <para>
+      This is an optional string that can be used to organize column master
+      keys into groups for lookup by clients.  The intent is that all column
+      master keys that are stored in the same system (file system location,
+      key management system, etc.) should be in the same realm.  A client
+      would then be configured to look up all keys in a given realm in a
+      certain way.  See the documentation of the respective client library for
+      further usage instructions.
+     </para>
+
+     <para>
+      The default is the empty string.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+<programlisting>
+CREATE COLUMN MASTER KEY cmk1 (realm = 'myrealm');
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index a03dee4afe..d1549c7f45 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -22,7 +22,7 @@
  <refsynopsisdiv>
 <synopsis>
 CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable> ( [
-  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,47 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables automatic client-side column-level encryption for the column.
+      <replaceable>encryption_options</replaceable> are comma-separated
+      <literal>key=value</literal> specifications.  The following options are
+      available:
+      <variablelist>
+       <varlistentry>
+        <term><literal>column_encryption_key</literal></term>
+        <listitem>
+         <para>
+          Specifies the name of the column encryption key to use.  Specifying
+          this is mandatory.  You must have <literal>USAGE</literal> privilege
+          on the column encryption key.
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>encryption_type</literal></term>
+        <listitem>
+         <para>
+          <literal>randomized</literal> (the default) or <literal>deterministic</literal>
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>algorithm</literal></term>
+        <listitem>
+         <para>
+          The encryption algorithm to use.  The default is
+          <literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -704,6 +745,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</literal></term>
         <listitem>
diff --git a/doc/src/sgml/ref/drop_column_encryption_key.sgml b/doc/src/sgml/ref/drop_column_encryption_key.sgml
new file mode 100644
index 0000000000..f2ac1beb08
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_encryption_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-encryption-key">
+ <indexterm zone="sql-drop-column-encryption-key">
+  <primary>DROP COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>remove a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN ENCRYPTION KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN ENCRYPTION KEY</command> removes a previously defined
+   column encryption key.  To be able to drop a column encryption key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column encryption key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column encryption key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column encryption key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column encryption key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN ENCRYPTION KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_column_master_key.sgml b/doc/src/sgml/ref/drop_column_master_key.sgml
new file mode 100644
index 0000000000..fae95e09d1
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_master_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-master-key">
+ <indexterm zone="sql-drop-column-master-key">
+  <primary>DROP COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN MASTER KEY</refname>
+  <refpurpose>remove a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN MASTER KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN MASTER KEY</command> removes a previously defined
+   column master key.  To be able to drop a column master key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column master key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column master key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column master key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column master key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN MASTER KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 35bf0332c8..f712f8e9e4 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -46,6 +46,16 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN ENCRYPTION KEY <replaceable>cek_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN MASTER KEY <replaceable>cmk_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON DOMAIN <replaceable>domain_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -513,7 +523,7 @@ <title>Compatibility</title>
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
+    Privileges on databases, tablespaces, schemas, keys, languages, and
     configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 2c938cd7e1..bb2e12148c 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -715,6 +715,48 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option turns on the column encryption connection option in
+        <application>libpq</application> (see <xref
+        linkend="libpq-connect-column-encryption"/>).  Column master key
+        lookup must be configured by the user, either through a connection
+        option or an environment setting (see <xref
+        linkend="libpq-connect-cmklookup"/>).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  (But then it is recommended
+        to not do this on the same host as the server, to avoid exposing
+        unencrypted data that is meant to be kept encrypted on the server.)
+        Note that a dump created with this option cannot be restored into a
+        database with column encryption.
+       </para>
+       <!--
+           XXX: The latter would require another pg_dump option to put out
+           INSERT ... \bind ... \g commands.
+       -->
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index e62d05e5ab..4bf60c729f 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -259,6 +259,33 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  Note that a dump created
+        with this option cannot be restored into a database with column
+        encryption.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index dc6528dc11..e68b8440be 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4026,6 +4054,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <term><varname>HIDE_COLUMN_ENCRYPTION</varname></term>
+        <listitem>
+        <para>
+         If this variable is set to <literal>true</literal>, column encryption
+         details are not displayed. This is mainly useful for regression
+         tests.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..c898997915 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -36,6 +36,8 @@ <title>SQL Commands</title>
    &abort;
    &alterAggregate;
    &alterCollation;
+   &alterColumnEncryptionKey;
+   &alterColumnMasterKey;
    &alterConversion;
    &alterDatabase;
    &alterDefaultPrivileges;
@@ -90,6 +92,8 @@ <title>SQL Commands</title>
    &createAggregate;
    &createCast;
    &createCollation;
+   &createColumnEncryptionKey;
+   &createColumnMasterKey;
    &createConversion;
    &createDatabase;
    &createDomain;
@@ -137,6 +141,8 @@ <title>SQL Commands</title>
    &dropAggregate;
    &dropCast;
    &dropCollation;
+   &dropColumnEncryptionKey;
+   &dropColumnMasterKey;
    &dropConversion;
    &dropDatabase;
    &dropDomain;
diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index ef818228ac..a00545080d 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -20,7 +20,9 @@
 
 #include "access/printsimple.h"
 #include "catalog/pg_type.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 
 /*
@@ -46,6 +48,11 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc)
 		pq_sendint16(&buf, attr->attlen);
 		pq_sendint32(&buf, attr->atttypmod);
 		pq_sendint16(&buf, 0);	/* format code */
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&buf, 0);	/* CEK */
+			pq_sendint32(&buf, 0);	/* CEK alg */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 72faeb5dfa..63a3d9b0f9 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,28 @@
  */
 #include "postgres.h"
 
+#include "access/genam.h"
 #include "access/printtup.h"
+#include "access/skey.h"
+#include "access/table.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
 #include "libpq/libpq.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memdebug.h"
 #include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
 
 
 static void printtup_startup(DestReceiver *self, int operation,
@@ -151,6 +166,156 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 	 */
 }
 
+/*
+ * Send ColumnMasterKey message, unless it's already been sent in this session
+ * for this key.
+ */
+List	   *cmk_sent = NIL;
+
+static void
+cmk_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+}
+
+static void
+MaybeSendColumnMasterKeyMessage(Oid cmkid)
+{
+	HeapTuple	tuple;
+	Form_pg_colmasterkey cmkform;
+	Datum		datum;
+	bool		isnull;
+	StringInfoData buf;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cmk_sent, cmkid))
+		return;
+
+	tuple = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	cmkform = (Form_pg_colmasterkey) GETSTRUCT(tuple);
+
+	pq_beginmessage(&buf, 'y'); /* ColumnMasterKey */
+	pq_sendint32(&buf, cmkform->oid);
+	pq_sendstring(&buf, NameStr(cmkform->cmkname));
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkrealm, &isnull);
+	Assert(!isnull);
+	pq_sendstring(&buf, TextDatumGetCString(datum));
+	pq_endmessage(&buf);
+
+	ReleaseSysCache(tuple);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CMKOID, cmk_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cmk_sent = lappend_oid(cmk_sent, cmkid);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * Send ColumnEncryptionKey message, unless it's already been sent in this
+ * session for this key.
+ */
+List	   *cek_sent = NIL;
+
+static void
+cek_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
+void
+MaybeSendColumnEncryptionKeyMessage(Oid attcek)
+{
+	HeapTuple	tuple;
+	ScanKeyData skey[1];
+	SysScanDesc sd;
+	Relation	rel;
+	bool		found = false;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cek_sent, attcek))
+		return;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	ScanKeyInit(&skey[0],
+				Anum_pg_colenckeydata_ckdcekid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(attcek));
+	rel = table_open(ColumnEncKeyDataRelationId, AccessShareLock);
+	sd = systable_beginscan(rel, ColumnEncKeyCekidCmkidIndexId, true, NULL, 1, skey);
+
+	while ((tuple = systable_getnext(sd)))
+	{
+		Form_pg_colenckeydata ckdform = (Form_pg_colenckeydata) GETSTRUCT(tuple);
+		Datum		datum;
+		bool		isnull;
+		bytea	   *ba;
+		StringInfoData buf;
+
+		MaybeSendColumnMasterKeyMessage(ckdform->ckdcmkid);
+
+		datum = heap_getattr(tuple, Anum_pg_colenckeydata_ckdencval, RelationGetDescr(rel), &isnull);
+		Assert(!isnull);
+		ba = pg_detoast_datum_packed((bytea *) DatumGetPointer(datum));
+
+		pq_beginmessage(&buf, 'Y'); /* ColumnEncryptionKey */
+		pq_sendint32(&buf, ckdform->ckdcekid);
+		pq_sendint32(&buf, ckdform->ckdcmkid);
+		pq_sendint32(&buf, ckdform->ckdcmkalg);
+		pq_sendint32(&buf, VARSIZE_ANY_EXHDR(ba));
+		pq_sendbytes(&buf, VARDATA_ANY(ba), VARSIZE_ANY_EXHDR(ba));
+		pq_endmessage(&buf);
+
+		found = true;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	systable_endscan(sd);
+	table_close(rel, NoLock);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CEKDATAOID, cek_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cek_sent = lappend_oid(cek_sent, attcek);
+	MemoryContextSwitchTo(oldcontext);
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -167,6 +332,7 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 						  List *targetlist, int16 *formats)
 {
 	int			natts = typeinfo->natts;
+	size_t		sz;
 	int			i;
 	ListCell   *tlist_item = list_head(targetlist);
 
@@ -183,14 +349,17 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 	 * Have to overestimate the size of the column-names, to account for
 	 * character set overhead.
 	 */
-	enlargeStringInfo(buf, (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
-							+ sizeof(Oid)	/* resorigtbl */
-							+ sizeof(AttrNumber)	/* resorigcol */
-							+ sizeof(Oid)	/* atttypid */
-							+ sizeof(int16) /* attlen */
-							+ sizeof(int32) /* attypmod */
-							+ sizeof(int16) /* format */
-							) * natts);
+	sz = (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
+		  + sizeof(Oid)	/* resorigtbl */
+		  + sizeof(AttrNumber)	/* resorigcol */
+		  + sizeof(Oid)	/* atttypid */
+		  + sizeof(int16) /* attlen */
+		  + sizeof(int32) /* attypmod */
+		  + sizeof(int16)); /* format */
+	if (MyProcPort->column_encryption_enabled)
+		sz += (sizeof(int32)		/* attcekid */
+			   + sizeof(int32));	/* attencalg */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +369,8 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +402,29 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(atttypid))
+		{
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(resorigtbl), Int16GetDatum(resorigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", resorigcol, resorigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			atttypid = orig_att->attusertypid;
+			atttypmod = orig_att->attusertypmod;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +432,11 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		pq_writeint16(buf, att->attlen);
 		pq_writeint32(buf, atttypmod);
 		pq_writeint16(buf, format);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_writeint32(buf, attcekid);
+			pq_writeint32(buf, attencalg);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +470,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 72a2c3d3db..f86ba299c3 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attusertypid != attr2->attusertypid)
+			return false;
+		if (attr1->attusertypmod != attr2->attusertypmod)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 24bab58499..cc48069932 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index a60107bf94..7b9575635b 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index c4232344aa..a3547c6cae 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_default_acl.h"
@@ -247,6 +249,12 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_SEQUENCE:
 			whole_mask = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			whole_mask = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			whole_mask = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			whole_mask = ACL_ALL_RIGHTS_DATABASE;
 			break;
@@ -473,6 +481,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
 			errormsg = gettext_noop("invalid privilege type %s for sequence");
 			break;
+		case OBJECT_CEK:
+			all_privileges = ACL_ALL_RIGHTS_CEK;
+			errormsg = gettext_noop("invalid privilege type %s for column encryption key");
+			break;
+		case OBJECT_CMK:
+			all_privileges = ACL_ALL_RIGHTS_CMK;
+			errormsg = gettext_noop("invalid privilege type %s for column master key");
+			break;
 		case OBJECT_DATABASE:
 			all_privileges = ACL_ALL_RIGHTS_DATABASE;
 			errormsg = gettext_noop("invalid privilege type %s for database");
@@ -597,6 +613,12 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_SEQUENCE:
 			ExecGrant_Relation(istmt);
 			break;
+		case OBJECT_CEK:
+			ExecGrant_common(istmt, ColumnEncKeyRelationId, ACL_ALL_RIGHTS_CEK, NULL);
+			break;
+		case OBJECT_CMK:
+			ExecGrant_common(istmt, ColumnMasterKeyRelationId, ACL_ALL_RIGHTS_CMK, NULL);
+			break;
 		case OBJECT_DATABASE:
 			ExecGrant_common(istmt, DatabaseRelationId, ACL_ALL_RIGHTS_DATABASE, NULL);
 			break;
@@ -676,6 +698,26 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 				objects = lappend_oid(objects, relOid);
 			}
 			break;
+		case OBJECT_CEK:
+			foreach(cell, objnames)
+			{
+				List	   *cekname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cek_oid(cekname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
+		case OBJECT_CMK:
+			foreach(cell, objnames)
+			{
+				List	   *cmkname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cmk_oid(cmkname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
 		case OBJECT_DATABASE:
 			foreach(cell, objnames)
 			{
@@ -2693,6 +2735,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("permission denied for column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("permission denied for column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -2798,6 +2846,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -2828,6 +2877,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("must be owner of aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("must be owner of column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("must be owner of column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("must be owner of collation %s");
 						break;
@@ -2938,6 +2993,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3019,6 +3075,10 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
+		case OBJECT_CEK:
+			return object_aclmask(ColumnEncKeyRelationId, object_oid, roleid, mask, how);
+		case OBJECT_CMK:
+			return object_aclmask(ColumnMasterKeyRelationId, object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
 		case OBJECT_FUNCTION:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..cab6cfd140 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -30,7 +30,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -153,6 +156,9 @@ static const Oid object_classes[] = {
 	TypeRelationId,				/* OCLASS_TYPE */
 	CastRelationId,				/* OCLASS_CAST */
 	CollationRelationId,		/* OCLASS_COLLATION */
+	ColumnEncKeyRelationId,		/* OCLASS_CEK */
+	ColumnEncKeyDataRelationId,	/* OCLASS_CEKDATA */
+	ColumnMasterKeyRelationId,	/* OCLASS_CMK */
 	ConstraintRelationId,		/* OCLASS_CONSTRAINT */
 	ConversionRelationId,		/* OCLASS_CONVERSION */
 	AttrDefaultRelationId,		/* OCLASS_DEFAULT */
@@ -1493,6 +1499,9 @@ doDeletion(const ObjectAddress *object, int flags)
 
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONVERSION:
 		case OCLASS_LANGUAGE:
 		case OCLASS_OPCLASS:
@@ -2859,6 +2868,15 @@ getObjectClass(const ObjectAddress *object)
 		case CollationRelationId:
 			return OCLASS_COLLATION;
 
+		case ColumnEncKeyRelationId:
+			return OCLASS_CEK;
+
+		case ColumnEncKeyDataRelationId:
+			return OCLASS_CEKDATA;
+
+		case ColumnMasterKeyRelationId:
+			return OCLASS_CMK;
+
 		case ConstraintRelationId:
 			return OCLASS_CONSTRAINT;
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 4f006820b8..df282c796f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypid - 1] = ObjectIdGetDatum(attrs->attusertypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypmod - 1] = Int32GetDatum(attrs->attusertypmod);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attusertypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attusertypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 14e57adee2..00f914bc5f 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -26,6 +26,8 @@
 #include "catalog/dependency.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1997,6 +1999,254 @@ OpfamilyIsVisible(Oid opfid)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4567,6 +4817,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(OpfamilyIsVisible(oid));
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2f688166e1..6c4ab9ac59 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -191,6 +194,48 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_COLLATION,
 		true
 	},
+	{
+		"column encryption key",
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		CEKOID,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		Anum_pg_colenckey_cekowner,
+		Anum_pg_colenckey_cekacl,
+		OBJECT_CEK,
+		true
+	},
+	{
+		"column encryption key data",
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		CEKDATAOID,
+		-1,
+		Anum_pg_colenckeydata_oid,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		-1,
+		false
+	},
+	{
+		"column master key",
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		CMKOID,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		Anum_pg_colmasterkey_cmkowner,
+		Anum_pg_colmasterkey_cmkacl,
+		OBJECT_CMK,
+		true
+	},
 	{
 		"constraint",
 		ConstraintRelationId,
@@ -723,6 +768,18 @@ static const struct object_type_map
 	{
 		"collation", OBJECT_COLLATION
 	},
+	/* OCLASS_CEK */
+	{
+		"column encryption key", OBJECT_CEK
+	},
+	/* OCLASS_CEKDATA */
+	{
+		"column encryption key data", OBJECT_CEKDATA
+	},
+	/* OCLASS_CMK */
+	{
+		"column master key", OBJECT_CMK
+	},
 	/* OCLASS_CONSTRAINT */
 	{
 		"table constraint", OBJECT_TABCONSTRAINT
@@ -1029,6 +1086,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1108,6 +1175,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, castNode(List, object));
+					Oid			cekid;
+					Oid			cmkid;
+
+					cekid = get_cek_oid(cekname, missing_ok);
+					cmkid = get_cmk_oid(cmkname, missing_ok);
+
+					address.classId = ColumnEncKeyDataRelationId;
+					address.objectId = get_cekdata_oid(cekid, cmkid, missing_ok);
+					address.objectSubId = 0;
+				}
+				break;
 			case OBJECT_TRANSFORM:
 				{
 					TypeName   *typename = linitial_node(TypeName, castNode(List, object));
@@ -2311,6 +2393,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2367,6 +2451,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2489,6 +2574,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2575,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3040,6 +3128,92 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CEKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata cekdata;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+
+				cekdata = (Form_pg_colenckeydata) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("column encryption key data of %s for %s"),
+								 getObjectDescription(&(ObjectAddress){ColumnEncKeyRelationId, cekdata->ckdcekid}, false),
+								 getObjectDescription(&(ObjectAddress){ColumnMasterKeyRelationId, cekdata->ckdcmkid}, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4440,6 +4614,18 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "collation");
 			break;
 
+		case OCLASS_CEK:
+			appendStringInfoString(&buffer, "column encryption key");
+			break;
+
+		case OCLASS_CEKDATA:
+			appendStringInfoString(&buffer, "column encryption key data");
+			break;
+
+		case OCLASS_CMK:
+			appendStringInfoString(&buffer, "column master key");
+			break;
+
 		case OCLASS_CONSTRAINT:
 			getConstraintTypeDescription(&buffer, object->objectId,
 										 missing_ok);
@@ -4905,6 +5091,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata form;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckeydata) GETSTRUCT(tup);
+				appendStringInfo(&buffer,
+								 "of %s for %s",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column master key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91..69f6175c60 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -19,6 +19,7 @@ OBJS = \
 	analyze.o \
 	async.o \
 	cluster.o \
+	colenccmds.o \
 	collationcmds.o \
 	comment.o \
 	constraint.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index bea51b3af1..59c23e9ef8 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -22,7 +22,9 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_foreign_data_wrapper.h"
@@ -118,6 +120,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -379,6 +387,8 @@ ExecRenameStmt(RenameStmt *stmt)
 			return RenameType(stmt);
 
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_EVENT_TRIGGER:
@@ -527,6 +537,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -643,6 +655,9 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 			break;
 
 		case OCLASS_CAST:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
@@ -876,6 +891,8 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/colenccmds.c b/src/backend/commands/colenccmds.c
new file mode 100644
index 0000000000..3ada6d5aeb
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,439 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/colenccmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+static void
+parse_cek_attributes(ParseState *pstate, List *definition, Oid *cmkoid_p, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	Assert(cmkoid_p);
+
+	foreach(lc, definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "column_master_key") == 0)
+			defelp = &cmkEl;
+		else if (strcmp(defel->defname, "algorithm") == 0)
+			defelp = &algEl;
+		else if (strcmp(defel->defname, "encrypted_value") == 0)
+			defelp = &encvalEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column encryption key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (cmkEl)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("attribute \"%s\" must be specified",
+						"column_master_key")));
+
+	if (algEl)
+	{
+		char	   *val = defGetString(algEl);
+
+		if (!alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"algorithm")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	if (encvalEl)
+	{
+		if (!encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"encrypted_value")));
+
+		encval = defGetString(encvalEl);
+	}
+	else
+	{
+		if (encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"encrypted_value")));
+	}
+
+	*cmkoid_p = cmkoid;
+	if (alg_p)
+		*alg_p = alg;
+	if (encval_p)
+		*encval_p = encval;
+}
+
+static void
+insert_cekdata_record(Oid cekoid, Oid cmkoid, int alg, char *encval)
+{
+	Oid			cekdataoid;
+	Relation	rel;
+	Datum		values[Natts_pg_colenckeydata] = {0};
+	bool		nulls[Natts_pg_colenckeydata] = {0};
+	HeapTuple	tup;
+	ObjectAddress myself;
+	ObjectAddress other;
+
+	rel = table_open(ColumnEncKeyDataRelationId, RowExclusiveLock);
+
+	cekdataoid = GetNewOidWithIndex(rel, ColumnEncKeyDataOidIndexId, Anum_pg_colenckeydata_oid);
+	values[Anum_pg_colenckeydata_oid - 1] = ObjectIdGetDatum(cekdataoid);
+	values[Anum_pg_colenckeydata_ckdcekid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckeydata_ckdcmkid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colenckeydata_ckdcmkalg - 1] = Int32GetDatum(alg);
+	values[Anum_pg_colenckeydata_ckdencval - 1] = DirectFunctionCall1(byteain, CStringGetDatum(encval));
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyDataRelationId, cekdataoid);
+
+	/* dependency cekdata -> cek */
+	ObjectAddressSet(other, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_AUTO);
+
+	/* dependency cekdata -> cmk */
+	ObjectAddressSet(other, ColumnMasterKeyRelationId, cmkoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_NORMAL);
+
+	table_close(rel, NoLock);
+}
+
+ObjectAddress
+CreateCEK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+	nulls[Anum_pg_colenckey_cekacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOnOwner(ColumnEncKeyRelationId, cekoid, GetUserId());
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnEncKeyRelationId, cekoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt)
+{
+	Oid			cekoid;
+	ObjectAddress address;
+
+	cekoid = get_cek_oid(stmt->cekname, false);
+
+	if (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(stmt->cekname));
+
+	if (stmt->isDrop)
+	{
+		Oid			cmkoid = 0;
+		Oid			cekdataoid;
+		ObjectAddress obj;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, NULL, NULL);
+		cekdataoid = get_cekdata_oid(cekoid, cmkoid, false);
+		ObjectAddressSet(obj, ColumnEncKeyDataRelationId, cekdataoid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+		AclResult	aclresult;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		if (get_cekdata_oid(cekoid, cmkoid, true))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("column encryption key \"%s\" already has data for master key \"%s\"",
+						   NameListToString(stmt->cekname), get_cmk_name(cmkoid, false)));
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	InvokeObjectPostAlterHook(ColumnEncKeyRelationId, cekoid, 0);
+	ObjectAddressSet(address, ColumnEncKeyRelationId, cekoid);
+
+	return address;
+}
+
+ObjectAddress
+CreateCMK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+		realm = defGetString(realmEl);
+	else
+		realm = "";
+
+	cmkoid = GetNewOidWithIndex(rel, ColumnMasterKeyOidIndexId, Anum_pg_colmasterkey_oid);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+	nulls[Anum_pg_colmasterkey_cmkacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	recordDependencyOnOwner(ColumnMasterKeyRelationId, cmkoid, GetUserId());
+
+	ObjectAddressSet(myself, ColumnMasterKeyRelationId, cmkoid);
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d6c6d514f3..9685b7886a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 82bda15889..b4c681005a 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +517,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..4c1628cf7b 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,9 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -1027,6 +1030,9 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_TYPE:
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_CONVERSION:
 		case OCLASS_DEFAULT:
@@ -2056,6 +2062,9 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2139,6 +2148,9 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 42cced9ebe..4b5ac30441 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..93d61c96ad 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,9 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 62d9917ca3..c30b59265b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,16 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			AclResult	aclresult;
+
+			GetColumnEncryption(colDef->encryption, attr);
+			aclresult = object_aclcheck(ColumnEncKeyRelationId, attr->attcek, GetUserId(), ACL_USAGE);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attr->attcek, false));
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -2562,13 +2575,43 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				Oid			defCollId;
 
 				/*
-				 * Yes, try to merge the two column definitions. They must
-				 * have the same type, typmod, and collation.
+				 * Yes, try to merge the two column definitions.
 				 */
 				ereport(NOTICE,
 						(errmsg("merging multiple inherited definitions of column \"%s\"",
 								attributeName)));
 				def = (ColumnDef *) list_nth(inhSchema, exist_attno - 1);
+
+				/*
+				 * Check encryption parameter.  All parents must have the same
+				 * encryption settings for a column.
+				 */
+				if ((def->encryption && !attribute->attcek) ||
+					(!def->encryption && attribute->attcek))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && attribute->attcek)
+				{
+					/*
+					 * Merging the encryption properties of two encrypted
+					 * parent columns is not yet implemented.  Right now, this
+					 * would confuse the checks of the type etc. below (we
+					 * must check the physical and the real types against each
+					 * other, respectively), which might require a larger
+					 * restructuring.  For now, just give up here.
+					 */
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("multiple inheritance of encrypted columns is not implemented")));
+				}
+
+				/*
+				 * Must have the same type, typmod, and collation.
+				 */
 				typenameTypeIdAndMod(NULL, def->typeName, &defTypeId, &deftypmod);
 				if (defTypeId != attribute->atttypid ||
 					deftypmod != attribute->atttypmod)
@@ -2641,6 +2684,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+														attribute->attusertypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -2919,6 +2968,34 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								 errdetail("%s versus %s", def->compression, newdef->compression)));
 				}
 
+				/*
+				 * Check encryption parameter.  All parents and children must
+				 * have the same encryption settings for a column.
+				 */
+				if ((def->encryption && !newdef->encryption) ||
+					(!def->encryption && newdef->encryption))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && newdef->encryption)
+				{
+					FormData_pg_attribute a, newa;
+
+					GetColumnEncryption(def->encryption, &a);
+					GetColumnEncryption(newdef->encryption, &newa);
+
+					if (a.atttypid != newa.atttypid ||
+						a.atttypmod != newa.atttypmod ||
+						a.attcek != newa.attcek)
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" has an encryption specification conflict",
+										attributeName)));
+				}
+
 				/* Mark the column as locally defined */
 				def->is_local = true;
 				/* Merge of NOT NULL constraints = OR 'em together */
@@ -6861,6 +6938,19 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attislocal = colDef->is_local;
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
+	if (colDef->encryption)
+	{
+		GetColumnEncryption(colDef->encryption, &attribute);
+		aclresult = object_aclcheck(ColumnEncKeyRelationId, attribute.attcek, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attribute.attcek, false));
+	}
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attusertypid = 0;
+		attribute.attusertypmod = -1;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12692,6 +12782,9 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
 			case OCLASS_COLLATION:
+			case OCLASS_CEK:
+			case OCLASS_CEKDATA:
+			case OCLASS_CMK:
 			case OCLASS_CONVERSION:
 			case OCLASS_LANGUAGE:
 			case OCLASS_LARGEOBJECT:
@@ -19291,3 +19384,110 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	List	   *cek = NULL;
+	Oid			cekoid;
+	bool		encdet = false;
+	int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+	foreach(lc, coldefencryption)
+	{
+		DefElem    *el = lfirst_node(DefElem, lc);
+
+		if (strcmp(el->defname, "column_encryption_key") == 0)
+			cek = defGetQualifiedName(el);
+		else if (strcmp(el->defname, "encryption_type") == 0)
+		{
+			char	   *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+			if (strcmp(val, "deterministic") == 0)
+				encdet = true;
+			else if (strcmp(val, "randomized") == 0)
+				encdet = false;
+			else
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption type: %s", val));
+		}
+		else if (strcmp(el->defname, "algorithm") == 0)
+		{
+			char	   *val = strVal(el->arg);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption algorithm: %s", val));
+		}
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized column encryption parameter: %s", el->defname));
+	}
+
+	if (!cek)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+				errmsg("column encryption key must be specified"));
+
+	cekoid = get_cek_oid(cek, false);
+
+	attr->attcek = cekoid;
+	attr->attusertypid = attr->atttypid;
+	attr->attusertypmod = attr->atttypmod;
+
+	/* override physical type */
+	if (encdet)
+		attr->atttypid = PG_ENCRYPTED_DETOID;
+	else
+		attr->atttypid = PG_ENCRYPTED_RNDOID;
+	get_typlenbyvalalign(attr->atttypid,
+						 &attr->attlen, &attr->attbyval, &attr->attalign);
+	attr->attstorage = get_typstorage(attr->atttypid);
+	attr->attcollation = InvalidOid;
+
+	attr->atttypmod = alg;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	List	   *result;
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	char	   *nspname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attr->attcek));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attr->attcek);
+
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+	nspname = get_namespace_name(form->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(form->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->atttypmod))),
+									-1));
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index bb0f5de4c2..cec2daa9c3 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -25,6 +25,7 @@
 #include "access/xlogprefetcher.h"
 #include "catalog/pg_authid.h"
 #include "common/string.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "postmaster/postmaster.h"
@@ -706,7 +707,11 @@ check_client_encoding(char **newval, void **extra, GucSource source)
 	 */
 	if (PrepareClientEncoding(encoding) < 0)
 	{
-		if (IsTransactionState())
+		if (MyProcPort->column_encryption_enabled)
+			GUC_check_errdetail("Conversion between %s and %s is not possible when column encryption is enabled.",
+								canonical_name,
+								GetDatabaseEncodingName());
+		else if (IsTransactionState())
 		{
 			/* Must be a genuine no-such-conversion problem */
 			GUC_check_errcode(ERRCODE_FEATURE_NOT_SUPPORTED);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index ff98c773f5..171dca3803 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index dc8415a693..c81bc15128 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3983,6 +3983,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(coldef->compression))
 					return true;
+				if (WALK(coldef->encryption))
+					return true;
 				if (WALK(coldef->raw_default))
 					return true;
 				if (WALK(coldef->collClause))
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a0138382a1..9f4b1e7d94 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,6 +420,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <list>	parse_toplevel stmtmulti routine_body_stmt_list
 				OptTableElementList TableElementList OptInherit definition
+				list_of_definitions
 				OptTypedTableElementList TypedTableElementList
 				reloptions opt_reloptions
 				OptWith opt_definition func_args func_args_list
@@ -592,6 +594,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableConstraint TableLikeClause
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression column_storage opt_column_storage
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_match
@@ -690,8 +693,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
 	DOUBLE_P DROP
 
-	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
-	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EACH ELSE ENABLE_P ENCODING ENCRYPTION ENCRYPTED END_P ENUM_P ESCAPE
+	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -714,7 +717,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
-	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+	MAPPING MASTER MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
 
 	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -3700,14 +3705,15 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_storage opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_encryption opt_column_storage opt_column_compression create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 
 					n->colname = $1;
 					n->typeName = $2;
-					n->storage_name = $3;
-					n->compression = $4;
+					n->encryption = $3;
+					n->storage_name = $4;
+					n->compression = $5;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3716,8 +3722,8 @@ columnDef:	ColId Typename opt_column_storage opt_column_compression create_gener
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $5;
-					SplitColQualList($6, &n->constraints, &n->collClause,
+					n->fdwoptions = $6;
+					SplitColQualList($7, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *) n;
@@ -3774,6 +3780,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4045,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6282,33 @@ DefineStmt:
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+			| CREATE COLUMN ENCRYPTION KEY any_name WITH VALUES list_of_definitions
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CEK;
+					n->defnames = $5;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name WITH definition
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = $7;
+					$$ = (Node *) n;
+				}
 		;
 
 definition: '(' def_list ')'						{ $$ = $2; }
@@ -6289,6 +6328,10 @@ def_elem:	ColLabel '=' def_arg
 				}
 		;
 
+list_of_definitions: definition						{ $$ = list_make1($1); }
+			| list_of_definitions ',' definition	{ $$ = lappend($1, $3); }
+		;
+
 /* Note: any simple identifier will be returned as a type name! */
 def_arg:	func_type						{ $$ = (Node *) $1; }
 			| reserved_keyword				{ $$ = (Node *) makeString(pstrdup($1)); }
@@ -6800,6 +6843,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -7611,6 +7656,24 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| COLUMN ENCRYPTION KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CEK;
+					n->objs = $4;
+					$$ = n;
+				}
+			| COLUMN MASTER KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CMK;
+					n->objs = $4;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9140,6 +9203,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9817,6 +9900,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10148,6 +10251,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11304,6 +11425,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_name ADD_P VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = false;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN ENCRYPTION KEY any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16958,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16854,6 +17022,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17506,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17425,6 +17595,7 @@ bare_label_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index 2240284f21..a8a1855aa0 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,159 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			Expr	   *expr = te->expr;
+
+			/*
+			 * If it's a RelabelType, look inside.  (For encrypted columns,
+			 * this would typically be a typmod adjustment.)
+			 */
+			if (IsA(expr, RelabelType))
+				expr = castNode(RelabelType, expr)->arg;
+
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(expr, Param))
+			{
+				Param	   *p = (Param *) expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(expr, Var))
+			{
+				Var		   *v = (Var *) expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell   *lc;
+
+					foreach(lc, srcrte->values_lists)
+					{
+						List	   *values_list = lfirst_node(List, lc);
+						Expr	   *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, RelabelType))
+							value = castNode(RelabelType, value)->arg;
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach(lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f9218f48aa..8d902292cd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1035,8 +1035,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+												attribute->attusertypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2552327d90..79ec9c4d1f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2210,12 +2210,27 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 									valptr),
 							 errhint("Valid values are: \"false\", 0, \"true\", 1, \"database\".")));
 			}
+			else if (strcmp(nameptr, "_pq_.column_encryption") == 0)
+			{
+				/*
+				 * Right now, the only accepted value is "1".  This gives room
+				 * to expand this into a version number, for example.
+				 */
+				if (strcmp(valptr, "1") == 0)
+					port->column_encryption_enabled = true;
+				else
+					ereport(FATAL,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for parameter \"%s\": \"%s\"",
+									"column_encryption",
+									valptr),
+							 errhint("Valid values are: 1.")));
+			}
 			else if (strncmp(nameptr, "_pq_.", 5) == 0)
 			{
 				/*
 				 * Any option beginning with _pq_. is reserved for use as a
-				 * protocol-level option, but at present no such options are
-				 * defined.
+				 * protocol-level option.
 				 */
 				unrecognized_protocol_options =
 					lappend(unrecognized_protocol_options, pstrdup(nameptr));
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index cab709b07b..c5acdd0dc2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -71,6 +72,7 @@
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
 
@@ -1815,6 +1817,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (type_is_encrypted(ptype))
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter $%d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2560,6 +2572,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2618,11 +2632,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	for (int i = 0; i < psrc->num_params; i++)
 	{
 		Oid			ptype = psrc->param_types[i];
+		Oid			pcekid = InvalidOid;
+		int			pcekalg = 0;
+		int16		pflags = 0;
+
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(ptype))
+		{
+			Oid			porigtbl = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("parameter $%d corresponds to an encrypted column, but an underlying table and column could not be determined", i + 1)));
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(porigtbl), Int16GetDatum(porigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", porigcol, porigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			ptype = orig_att->attusertypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x01;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&row_description_buf, (int) pcekid);
+			pq_sendint32(&row_description_buf, pcekalg);
+			pq_sendint16(&row_description_buf, pflags);
+		}
 	}
 	pq_endmessage_reuse(&row_description_buf);
 
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index c7d9d96b45..0a17eb4486 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -30,6 +30,7 @@
 #include "commands/alter.h"
 #include "commands/async.h"
 #include "commands/cluster.h"
+#include "commands/colenccmds.h"
 #include "commands/collationcmds.h"
 #include "commands/comment.h"
 #include "commands/conversioncmds.h"
@@ -137,6 +138,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,14 @@ ProcessUtilitySlow(ParseState *pstate,
 															stmt->definition,
 															&secondaryObject);
 							break;
+						case OBJECT_CEK:
+							Assert(stmt->args == NIL);
+							address = CreateCEK(pstate, stmt);
+							break;
+						case OBJECT_CMK:
+							Assert(stmt->args == NIL);
+							address = CreateCMK(pstate, stmt);
+							break;
 						case OBJECT_COLLATION:
 							Assert(stmt->args == NIL);
 							address = DefineCollation(pstate,
@@ -1903,6 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,12 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_COLUMN:
 			tag = CMDTAG_ALTER_TABLE;
 			break;
+		case OBJECT_CEK:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+		case OBJECT_CMK:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
 		case OBJECT_CONVERSION:
 			tag = CMDTAG_ALTER_CONVERSION;
 			break;
@@ -2640,6 +2665,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_DROP_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_DROP_COLUMN_MASTER_KEY;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2760,6 +2791,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_COLLATION:
 					tag = CMDTAG_CREATE_COLLATION;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_CREATE_COLUMN_MASTER_KEY;
+					break;
 				case OBJECT_ACCESS_METHOD:
 					tag = CMDTAG_CREATE_ACCESS_METHOD;
 					break;
@@ -3063,6 +3100,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3733,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 8f7522d103..b934f16074 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -800,6 +800,14 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			/* for backwards compatibility, grant some rights by default */
 			world_default = ACL_CREATE_TEMP | ACL_CONNECT;
@@ -911,6 +919,12 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 's':
 			objtype = OBJECT_SEQUENCE;
 			break;
+		case 'Y':
+			objtype = OBJECT_CEK;
+			break;
+		case 'y':
+			objtype = OBJECT_CMK;
+			break;
 		case 'd':
 			objtype = OBJECT_DATABASE;
 			break;
@@ -2915,6 +2929,10 @@ convert_column_priv_string(text *priv_type_text)
 }
 
 
+// TODO: has_column_encryption_key_privilege variants
+// TODO: has_column_master_key_privilege variants
+
+
 /*
  * has_database_privilege variants
  *		These are all named "has_database_privilege" at the SQL level.
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 170b3a3820..faf5a68849 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -679,6 +679,112 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c07382051d..7ad159110f 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -24,7 +24,10 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_namespace.h"
@@ -2658,6 +2661,25 @@ type_is_multirange(Oid typid)
 	return (get_typtype(typid) == TYPTYPE_MULTIRANGE);
 }
 
+bool
+type_is_encrypted(Oid typid)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+		bool		result;
+
+		result = (typtup->typcategory == TYPCATEGORY_ENCRYPTED);
+		ReleaseSysCache(tp);
+		return result;
+	}
+	else
+		return false;
+}
+
 /*
  * get_type_category_preferred
  *
@@ -3683,3 +3705,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+char *
+get_cek_name(Oid cekid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cekname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+		return NULL;
+	}
+
+	cekname = pstrdup(NameStr(((Form_pg_colenckey) GETSTRUCT(tup))->cekname));
+
+	ReleaseSysCache(tup);
+
+	return cekname;
+}
+
+Oid
+get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok)
+{
+	Oid			cekdataid;
+
+	cekdataid = GetSysCacheOid2(CEKDATACEKCMK, Anum_pg_colenckeydata_oid,
+								ObjectIdGetDatum(cekid),
+								ObjectIdGetDatum(cmkid));
+	if (!OidIsValid(cekdataid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" has no data for master key \"%s\"",
+						get_cek_name(cekid, false), get_cmk_name(cmkid, false))));
+
+	return cekdataid;
+}
+
+char *
+get_cmk_name(Oid cmkid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cmkname;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+		return NULL;
+	}
+
+	cmkname = pstrdup(NameStr(((Form_pg_colmasterkey) GETSTRUCT(tup))->cmkname));
+
+	ReleaseSysCache(tup);
+
+	return cmkname;
+}
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 77c2ba3f8f..d40af13efe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 94abede512..eb432260da 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -219,6 +222,32 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAMENSP] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameNspIndexId,
+		KEY(Anum_pg_colenckey_cekname,
+			Anum_pg_colenckey_ceknamespace),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +262,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAMENSP] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameNspIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname,
+			Anum_pg_colmasterkey_cmknamespace),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 033647011b..4b6be68f27 100644
--- a/src/backend/utils/mb/mbutils.c
+++ b/src/backend/utils/mb/mbutils.c
@@ -36,7 +36,9 @@
 
 #include "access/xact.h"
 #include "catalog/namespace.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
@@ -130,6 +132,12 @@ PrepareClientEncoding(int encoding)
 		encoding == PG_SQL_ASCII)
 		return 0;
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	if (IsTransactionState())
 	{
 		/*
@@ -237,6 +245,12 @@ SetClientEncoding(int encoding)
 		return 0;
 	}
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	/*
 	 * Search the cache for the entry previously prepared by
 	 * PrepareClientEncoding; if there isn't one, we lose.  While at it,
@@ -297,7 +311,9 @@ InitializeClientEncoding(void)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("conversion between %s and %s is not supported",
 						pg_enc2name_tbl[pending_client_encoding].name,
-						GetDatabaseEncodingName())));
+						GetDatabaseEncodingName()),
+				 (MyProcPort->column_encryption_enabled) ?
+				 errdetail("Encoding conversion is not possible when column encryption is enabled.") : 0));
 	}
 
 	/*
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index a43f2e5553..e67462c81a 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -201,6 +203,12 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading user-defined collations");
 	(void) getCollations(fout, &numCollations);
 
+	pg_log_info("reading column master keys");
+	getColumnMasterKeys(fout);
+
+	pg_log_info("reading column encryption keys");
+	getColumnEncryptionKeys(fout);
+
 	pg_log_info("reading user-defined conversions");
 	getConversions(fout, &numConversions);
 
@@ -859,6 +867,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 9753a6d868..0a5178bcf1 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -484,6 +484,10 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('U', "USAGE");
 	}
+	else if (strcmp(type, "COLUMN ENCRYPTION KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "COLUMN MASTER KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "DATABASE") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,7 @@ typedef struct _connParams
 	char	   *pghost;
 	char	   *username;
 	trivalue	promptPassword;
+	int			column_encryption;
 	/* If not NULL, this overrides the dbname obtained from command line */
 	/* (but *only* the DB name, not anything else in the connstring) */
 	char	   *override_dbname;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 269bfce019..baf0de3347 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3429,6 +3429,8 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 
 	/* objects that don't require special decoration */
 	if (strcmp(type, "COLLATION") == 0 ||
+		strcmp(type, "COLUMN ENCRYPTION KEY") == 0 ||
+		strcmp(type, "COLUMN MASTER KEY") == 0 ||
 		strcmp(type, "CONVERSION") == 0 ||
 		strcmp(type, "DOMAIN") == 0 ||
 		strcmp(type, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 100644
--- a/src/bin/pg_dump/pg_backup_db.c
+++ b/src/bin/pg_dump/pg_backup_db.c
@@ -133,8 +133,8 @@ ConnectDatabase(Archive *AHX,
 	 */
 	do
 	{
-		const char *keywords[8];
-		const char *values[8];
+		const char *keywords[9];
+		const char *values[9];
 		int			i = 0;
 
 		/*
@@ -159,6 +159,11 @@ ConnectDatabase(Archive *AHX,
 		}
 		keywords[i] = "fallback_application_name";
 		values[i++] = progname;
+		if (cparams->column_encryption)
+		{
+			keywords[i] = "column_encryption";
+			values[i++] = "1";
+		}
 		keywords[i] = NULL;
 		values[i++] = NULL;
 		Assert(i <= lengthof(keywords));
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 1a06eeaf6a..2fd7e51938 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,8 @@ static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
 static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
 static void dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo);
 static void dumpCollation(Archive *fout, const CollInfo *collinfo);
+static void dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo);
+static void dumpColumnMasterKey(Archive *fout, const CmkInfo *cekinfo);
 static void dumpConversion(Archive *fout, const ConvInfo *convinfo);
 static void dumpRule(Archive *fout, const RuleInfo *rinfo);
 static void dumpAgg(Archive *fout, const AggInfo *agginfo);
@@ -393,6 +396,7 @@ main(int argc, char **argv)
 		{"attribute-inserts", no_argument, &dopt.column_inserts, 1},
 		{"binary-upgrade", no_argument, &dopt.binary_upgrade, 1},
 		{"column-inserts", no_argument, &dopt.column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &dopt.cparams.column_encryption, 1},
 		{"disable-dollar-quoting", no_argument, &dopt.disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &dopt.disable_triggers, 1},
 		{"enable-row-security", no_argument, &dopt.enable_row_security, 1},
@@ -685,6 +689,9 @@ main(int argc, char **argv)
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
 	 */
+	if (dopt.cparams.column_encryption && dopt.dump_inserts == 0)
+		pg_fatal("option --decrypt-encrypted-columns requires option --inserts, --rows-per-insert, or --column-inserts");
+
 	if (dopt.do_nothing && dopt.dump_inserts == 0)
 		pg_fatal("option --on-conflict-do-nothing requires option --inserts, --rows-per-insert, or --column-inserts");
 
@@ -1057,6 +1064,7 @@ help(const char *progname)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security (dump only content user has\n"
@@ -5572,6 +5580,164 @@ getCollations(Archive *fout, int *numCollations)
 	return collinfo;
 }
 
+/*
+ * getColumnEncryptionKeys
+ *	  get information about column encryption keys
+ */
+void
+getColumnEncryptionKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CekInfo	   *cekinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cekname;
+	int			i_ceknamespace;
+	int			i_cekowner;
+	int			i_cekacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, cek.cekowner, cek.cekacl, acldefault('Y', cek.cekowner) AS acldefault\n"
+					  "FROM pg_colenckey cek");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cekname = PQfnumber(res, "cekname");
+	i_ceknamespace = PQfnumber(res, "ceknamespace");
+	i_cekowner = PQfnumber(res, "cekowner");
+	i_cekacl = PQfnumber(res, "cekacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cekinfo = pg_malloc(ntups * sizeof(CekInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		PGresult	   *res2;
+		int				ntups2;
+
+		cekinfo[i].dobj.objType = DO_CEK;
+		cekinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cekinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cekinfo[i].dobj);
+		cekinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cekname));
+		cekinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cekacl));
+		cekinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cekinfo[i].dacl.privtype = 0;
+		cekinfo[i].dacl.initprivs = NULL;
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, ckdcmkalg, ckdencval\n"
+						  "FROM pg_catalog.pg_colenckeydata\n"
+						  "WHERE ckdcekid = %u", cekinfo[i].dobj.catId.oid);
+		res2 = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		ntups2 = PQntuples(res2);
+		cekinfo[i].numdata = ntups2;
+		cekinfo[i].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			cekinfo[i].cekcmkalgs[j] = atoi(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkalg")));
+			cekinfo[i].cekencvals[j] = pg_strdup(PQgetvalue(res2, j, PQfnumber(res2, "ckdencval")));
+		}
+		PQclear(res2);
+
+		selectDumpableObject(&(cekinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cekacl))
+			cekinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * getColumnMasterKeys
+ *	  get information about column master keys
+ */
+void
+getColumnMasterKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CmkInfo	   *cmkinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cmkname;
+	int			i_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+	int			i_cmkacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, cmk.cmkowner, cmk.cmkrealm, cmk.cmkacl, acldefault('y', cmk.cmkowner) AS acldefault\n"
+					  "FROM pg_colmasterkey cmk");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cmkname = PQfnumber(res, "cmkname");
+	i_cmknamespace = PQfnumber(res, "cmknamespace");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+	i_cmkacl = PQfnumber(res, "cmkacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cmkinfo = pg_malloc(ntups * sizeof(CmkInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		cmkinfo[i].dobj.objType = DO_CMK;
+		cmkinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cmkinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cmkinfo[i].dobj);
+		cmkinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cmkname));
+		cmkinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		cmkinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cmkacl));
+		cmkinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cmkinfo[i].dacl.privtype = 0;
+		cmkinfo[i].dacl.initprivs = NULL;
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cmkacl))
+			cmkinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8187,6 +8353,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8246,8 +8415,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
+	appendPQExpBuffer(q, "SELECT\n"
 						 "a.attrelid,\n"
 						 "a.attnum,\n"
 						 "a.attname,\n"
@@ -8260,7 +8428,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attlen,\n"
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
+						 "pg_catalog.format_type(%s) AS atttypname,\n"
 						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
@@ -8269,7 +8437,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "' ' || pg_catalog.quote_literal(option_value) "
 						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
 						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+						 "), E',\n    ') AS attfdwoptions,\n",
+						 fout->remoteVersion >= 160000 ?
+						 "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END, CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END" :
+						 "a.atttypid, a.atttypmod");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -8295,10 +8466,23 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	if (fout->remoteVersion >= 120000)
 		appendPQExpBufferStr(q,
-							 "a.attgenerated\n");
+							 "a.attgenerated,\n");
+	else
+		appendPQExpBufferStr(q,
+							 "'' AS attgenerated,\n");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBuffer(q,
+						  "a.attcek,\n"
+						  "CASE a.atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "CASE WHEN a.atttypid IN (%u, %u) THEN a.atttypmod END AS attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID,
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcek,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
@@ -8323,6 +8507,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	i_attencdet = PQfnumber(res, "attencdet");
+	i_attencalg = PQfnumber(res, "attencalg");
 	i_attisdropped = PQfnumber(res, "attisdropped");
 	i_attlen = PQfnumber(res, "attlen");
 	i_attalign = PQfnumber(res, "attalign");
@@ -8383,6 +8570,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		tbinfo->typstorage = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attidentity = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attgenerated = (char *) pg_malloc(numatts * sizeof(char));
+		tbinfo->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		tbinfo->attencdet = (bool *) pg_malloc(numatts * sizeof(bool));
+		tbinfo->attencalg = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char));
@@ -8410,6 +8600,22 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->attidentity[j] = *(PQgetvalue(res, r, i_attidentity));
 			tbinfo->attgenerated[j] = *(PQgetvalue(res, r, i_attgenerated));
 			tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
+			if (!PQgetisnull(res, r, i_attcek))
+			{
+				Oid		attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[j] = NULL;
+			if (!PQgetisnull(res, r, i_attencdet))
+				tbinfo->attencdet[j] = (PQgetvalue(res, r, i_attencdet)[0] == 't');
+			else
+				tbinfo->attencdet[j] = 0;
+			if (!PQgetisnull(res, r, i_attencalg))
+				tbinfo->attencalg[j] = atoi(PQgetvalue(res, r, i_attencalg));
+			else
+				tbinfo->attencalg[j] = 0;
 			tbinfo->attisdropped[j] = (PQgetvalue(res, r, i_attisdropped)[0] == 't');
 			tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
 			tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
@@ -9928,6 +10134,12 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_OPFAMILY:
 			dumpOpfamily(fout, (const OpfamilyInfo *) dobj);
 			break;
+		case DO_CEK:
+			dumpColumnEncryptionKey(fout, (const CekInfo *) dobj);
+			break;
+		case DO_CMK:
+			dumpColumnMasterKey(fout, (const CmkInfo *) dobj);
+			break;
 		case DO_COLLATION:
 			dumpCollation(fout, (const CollInfo *) dobj);
 			break;
@@ -13321,6 +13533,141 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * dumpColumnEncryptionKey
+ *	  dump the definition of the given column encryption key
+ */
+static void
+dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcekname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcekname = pg_strdup(fmtId(cekinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN ENCRYPTION KEY %s;\n",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_name(cekinfo->cekcmkalgs[i]));
+		appendPQExpBuffer(query, "encrypted_value = ");
+		appendStringLiteralAH(query, cekinfo->cekencvals[i], fout);
+
+		appendPQExpBuffer(query, ")");
+		if (i < cekinfo->numdata - 1)
+			appendPQExpBuffer(query, ", ");
+	}
+
+	appendPQExpBufferStr(query, ";\n");
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cekinfo->dobj.catId, cekinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cekinfo->dobj.name,
+								  .namespace = cekinfo->dobj.namespace->dobj.name,
+								  .owner = cekinfo->rolname,
+								  .description = "COLUMN ENCRYPTION KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cekinfo->dobj.dumpId, InvalidDumpId, "COLUMN ENCRYPTION KEY",
+				qcekname, NULL, cekinfo->dobj.namespace->dobj.name,
+				cekinfo->rolname, &cekinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcekname);
+}
+
+/*
+ * dumpColumnMasterKey
+ *	  dump the definition of the given column master key
+ */
+static void
+dumpColumnMasterKey(Archive *fout, const CmkInfo *cmkinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcmkname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcmkname = pg_strdup(fmtId(cmkinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN MASTER KEY %s;\n",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "realm = ");
+	appendStringLiteralAH(query, cmkinfo->cmkrealm, fout);
+
+	appendPQExpBufferStr(query, ");\n");
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cmkinfo->dobj.catId, cmkinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cmkinfo->dobj.name,
+								  .namespace = cmkinfo->dobj.namespace->dobj.name,
+								  .owner = cmkinfo->rolname,
+								  .description = "COLUMN MASTER KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN MASTER KEY", qcmkname,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cmkinfo->dobj.dumpId, InvalidDumpId, "COLUMN MASTER KEY",
+				qcmkname, NULL, cmkinfo->dobj.namespace->dobj.name,
+				cmkinfo->rolname, &cmkinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15404,6 +15751,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[j]));
+						/*
+						 * To reduce output size, we don't print the default
+						 * of encryption_type, but we do print the default of
+						 * algorithm, since we might want to change to a new
+						 * default algorithm sometime in the future.
+						 */
+						if (tbinfo->attencdet[j])
+							appendPQExpBuffer(q, "encryption_type = deterministic, ");
+						appendPQExpBuffer(q, "algorithm = '%s')",
+										  get_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -17967,6 +18330,8 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_ACCESS_METHOD:
 			case DO_OPCLASS:
 			case DO_OPFAMILY:
+			case DO_CEK:
+			case DO_CMK:
 			case DO_COLLATION:
 			case DO_CONVERSION:
 			case DO_TABLE:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index cdca0b993d..d4a2e595d0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -47,6 +47,8 @@ typedef enum
 	DO_ACCESS_METHOD,
 	DO_OPCLASS,
 	DO_OPFAMILY,
+	DO_CEK,
+	DO_CMK,
 	DO_COLLATION,
 	DO_CONVERSION,
 	DO_TABLE,
@@ -332,6 +334,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	struct _CekInfo **attcek;
+	int		   *attencalg;
+	bool	   *attencdet;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
 	char	   *attalign;		/* attribute align, used by binary_upgrade */
 	bool	   *attislocal;		/* true if attr has local definition */
@@ -663,6 +668,32 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	struct _CmkInfo **cekcmks;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -683,6 +714,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -710,6 +743,8 @@ extern AccessMethodInfo *getAccessMethods(Archive *fout, int *numAccessMethods);
 extern OpclassInfo *getOpclasses(Archive *fout, int *numOpclasses);
 extern OpfamilyInfo *getOpfamilies(Archive *fout, int *numOpfamilies);
 extern CollInfo *getCollations(Archive *fout, int *numCollations);
+extern void getColumnEncryptionKeys(Archive *fout);
+extern void getColumnMasterKeys(Archive *fout);
 extern ConvInfo *getConversions(Archive *fout, int *numConversions);
 extern TableInfo *getTables(Archive *fout, int *numTables);
 extern void getOwnedSeqs(Archive *fout, TableInfo tblinfo[], int numTables);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 8266c117a3..d3dacd39da 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -69,6 +69,8 @@ enum dbObjectTypePriorities
 	PRIO_TSTEMPLATE,
 	PRIO_TSDICT,
 	PRIO_TSCONFIG,
+	PRIO_CMK,
+	PRIO_CEK,
 	PRIO_FDW,
 	PRIO_FOREIGN_SERVER,
 	PRIO_TABLE,
@@ -111,6 +113,8 @@ static const int dbObjectTypePriority[] =
 	PRIO_ACCESS_METHOD,			/* DO_ACCESS_METHOD */
 	PRIO_OPFAMILY,				/* DO_OPCLASS */
 	PRIO_OPFAMILY,				/* DO_OPFAMILY */
+	PRIO_CEK,					/* DO_CEK */
+	PRIO_CMK,					/* DO_CMK */
 	PRIO_COLLATION,				/* DO_COLLATION */
 	PRIO_CONVERSION,			/* DO_CONVERSION */
 	PRIO_TABLE,					/* DO_TABLE */
@@ -1322,6 +1326,16 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "OPERATOR FAMILY %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_CEK:
+			snprintf(buf, bufsize,
+					 "COLUMN ENCRYPTION KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
+		case DO_CMK:
+			snprintf(buf, bufsize,
+					 "COLUMN MASTER KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_COLLATION:
 			snprintf(buf, bufsize,
 					 "COLLATION %s  (ID %d OID %u)",
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index cd421c5944..6530fb81a2 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -93,6 +93,7 @@ static bool dosync = true;
 
 static int	binary_upgrade = 0;
 static int	column_inserts = 0;
+static int	decrypt_encrypted_columns = 0;
 static int	disable_dollar_quoting = 0;
 static int	disable_triggers = 0;
 static int	if_exists = 0;
@@ -154,6 +155,7 @@ main(int argc, char *argv[])
 		{"attribute-inserts", no_argument, &column_inserts, 1},
 		{"binary-upgrade", no_argument, &binary_upgrade, 1},
 		{"column-inserts", no_argument, &column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &decrypt_encrypted_columns, 1},
 		{"disable-dollar-quoting", no_argument, &disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &disable_triggers, 1},
 		{"exclude-database", required_argument, NULL, 6},
@@ -424,6 +426,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --binary-upgrade");
 	if (column_inserts)
 		appendPQExpBufferStr(pgdumpopts, " --column-inserts");
+	if (decrypt_encrypted_columns)
+		appendPQExpBufferStr(pgdumpopts, " --decrypt-encrypted-columns");
 	if (disable_dollar_quoting)
 		appendPQExpBufferStr(pgdumpopts, " --disable-dollar-quoting");
 	if (disable_triggers)
@@ -649,6 +653,7 @@ help(void)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --exclude-database=PATTERN   exclude databases whose name matches PATTERN\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index d92247c915..f4f13fd087 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -645,6 +645,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
 	'ALTER FOREIGN DATA WRAPPER dummy OWNER TO' => {
 		regexp => qr/^ALTER FOREIGN DATA WRAPPER dummy OWNER TO .+;/m,
 		like   => { %full_runs, section_pre_data => 1, },
@@ -1245,6 +1257,26 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1663,6 +1695,26 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3496,6 +3548,26 @@
 		unlike => { no_privs => 1, },
 	},
 
+	'GRANT USAGE ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
+	'GRANT USAGE ON COLUMN MASTER KEY cmk1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
 	'GRANT USAGE ON FOREIGN DATA WRAPPER dummy' => {
 		create_order => 85,
 		create_sql   => 'GRANT USAGE ON FOREIGN DATA WRAPPER dummy
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..0d6a46d24b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c8a0bb7b3a..04d437f836 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1530,7 +1530,7 @@ describeOneTableDetails(const char *schemaname,
 	bool		printTableInitialized = false;
 	int			i;
 	char	   *view_def = NULL;
-	char	   *headers[12];
+	char	   *headers[13];
 	PQExpBufferData title;
 	PQExpBufferData tmpbuf;
 	int			cols;
@@ -1546,6 +1546,7 @@ describeOneTableDetails(const char *schemaname,
 				fdwopts_col = -1,
 				attstorage_col = -1,
 				attcompression_col = -1,
+				attcekname_col = -1,
 				attstattarget_col = -1,
 				attdescr_col = -1;
 	int			numrows;
@@ -1568,6 +1569,8 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attusertypid;
+	const char *attusertypmod;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1844,7 +1847,17 @@ describeOneTableDetails(const char *schemaname,
 	cols = 0;
 	printfPQExpBuffer(&buf, "SELECT a.attname");
 	attname_col = cols++;
-	appendPQExpBufferStr(&buf, ",\n  pg_catalog.format_type(a.atttypid, a.atttypmod)");
+	if (pset.sversion >= 160000)
+	{
+		attusertypid = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END";
+		attusertypmod = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END";
+	}
+	else
+	{
+		attusertypid = "a.atttypid";
+		attusertypmod = "a.atttypmod";
+	}
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, %s)", attusertypid, attusertypmod);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1857,7 +1870,8 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		attrdef_col = cols++;
 		attnotnull_col = cols++;
-		appendPQExpBufferStr(&buf, ",\n  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
+		appendPQExpBufferStr(&buf, ",\n"
+							 "  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
 							 "   WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) AS attcollation");
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
@@ -1909,6 +1923,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			appendPQExpBufferStr(&buf, ",\n  (SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek) AS attcekname");
+			attcekname_col = cols++;
+		}
+
 		/* stats target, if relevant to relkind */
 		if (tableinfo.relkind == RELKIND_RELATION ||
 			tableinfo.relkind == RELKIND_INDEX ||
@@ -2032,6 +2058,8 @@ describeOneTableDetails(const char *schemaname,
 		headers[cols++] = gettext_noop("Storage");
 	if (attcompression_col >= 0)
 		headers[cols++] = gettext_noop("Compression");
+	if (attcekname_col >= 0)
+		headers[cols++] = gettext_noop("Encryption");
 	if (attstattarget_col >= 0)
 		headers[cols++] = gettext_noop("Stats target");
 	if (attdescr_col >= 0)
@@ -2124,6 +2152,17 @@ describeOneTableDetails(const char *schemaname,
 							  false, false);
 		}
 
+		/* Column encryption */
+		if (attcekname_col >= 0)
+		{
+			if (!PQgetisnull(res, i, attcekname_col))
+				printTableAddCell(&cont, PQgetvalue(res, i, attcekname_col),
+								  false, false);
+			else
+				printTableAddCell(&cont, "",
+								  false, false);
+		}
+
 		/* Statistics target, if the relkind supports this feature */
 		if (attstattarget_col >= 0)
 			printTableAddCell(&cont, PQgetvalue(res, i, attstattarget_col),
@@ -4477,6 +4516,152 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cekacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cmkacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cmk.oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 554fe86725..1cf8f72176 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1729966959 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,8 @@ helpVariables(unsigned short int pager)
 		  "    true if last query failed, else false\n");
 	HELP0("  FETCH_COUNT\n"
 		  "    the number of result rows to fetch and display at a time (0 = unlimited)\n");
+	HELP0("  HIDE_COLUMN_ENCRYPTION\n"
+		  "    if set, column encryption details are not displayed\n");
 	HELP0("  HIDE_TABLEAM\n"
 		  "    if set, table access methods are not displayed\n");
 	HELP0("  HIDE_TOAST_COMPRESSION\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 73d4b393bc..010bc5a6d5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,7 @@ typedef struct _psqlSettings
 	bool		quiet;
 	bool		singleline;
 	bool		singlestep;
+	bool		hide_column_encryption;
 	bool		hide_compression;
 	bool		hide_tableam;
 	int			fetch_count;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 5a28b6f713..6736505c3a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -1188,6 +1188,13 @@ hide_compression_hook(const char *newval)
 							 &pset.hide_compression);
 }
 
+static bool
+hide_column_encryption_hook(const char *newval)
+{
+	return ParseVariableBool(newval, "HIDE_COLUMN_ENCRYPTION",
+							 &pset.hide_column_encryption);
+}
+
 static bool
 hide_tableam_hook(const char *newval)
 {
@@ -1259,6 +1266,9 @@ EstablishVariableSpace(void)
 	SetVariableHooks(pset.vars, "SHOW_CONTEXT",
 					 show_context_substitute_hook,
 					 show_context_hook);
+	SetVariableHooks(pset.vars, "HIDE_COLUMN_ENCRYPTION",
+					 bool_substitute_hook,
+					 hide_column_encryption_hook);
 	SetVariableHooks(pset.vars, "HIDE_TOAST_COMPRESSION",
 					 bool_substitute_hook,
 					 hide_compression_hook);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5e1882eaea..72add64773 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -933,6 +933,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1223,6 +1237,8 @@ static const pgsql_thing_t words_after_create[] = {
 	{"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", NULL, NULL, &Query_for_list_of_collations},
+	{"COLUMN ENCRYPTION KEY", NULL, NULL, NULL},
+	{"COLUMN MASTER KEY KEY", NULL, NULL, NULL},
 
 	/*
 	 * CREATE CONSTRAINT TRIGGER is not supported here because it is designed
@@ -1705,7 +1721,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1952,6 +1968,22 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "COLLATION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "REFRESH VERSION", "RENAME TO", "SET SCHEMA");
 
+	/* ALTER/DROP COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "ENCRYPTION", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_ceks);
+
+	/* ALTER COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("ADD VALUE (", "DROP VALUE (", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2894,6 +2926,26 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("true", "false");
 	}
 
+	/* CREATE/ALTER/DROP COLUMN ... KEY */
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN"))
+		COMPLETE_WITH("ENCRYPTION", "MASTER");
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN", "ENCRYPTION|MASTER"))
+		COMPLETE_WITH("KEY");
+
+	/* CREATE COLUMN ENCRYPTION KEY */
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("VALUES");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH", "VALUES"))
+		COMPLETE_WITH("(");
+
+	/* CREATE COLUMN MASTER KEY */
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("(");
+
 	/* CREATE DATABASE */
 	else if (Matches("CREATE", "DATABASE", MatchAny))
 		COMPLETE_WITH("OWNER", "TEMPLATE", "ENCODING", "TABLESPACE",
@@ -3619,6 +3671,7 @@ psql_completion(const char *text, int start, int end)
 			 Matches("DROP", "ACCESS", "METHOD", MatchAny) ||
 			 (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
 			  ends_with(prev_wd, ')')) ||
+			 Matches("DROP", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
 			 Matches("DROP", "EVENT", "TRIGGER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "TABLE", MatchAny) ||
@@ -3931,6 +3984,8 @@ psql_completion(const char *text, int start, int end)
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"COLUMN ENCRYPTION KEY",
+											"COLUMN MASTER KEY",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4046,6 +4101,16 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+	/* Complete "GRANT/REVOKE * ON COLUMN ENCRYPTION|MASTER KEY *" with TO/FROM */
+	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
+			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny))
+	{
+		if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny, MatchAny))
+			COMPLETE_WITH("TO");
+		else
+			COMPLETE_WITH("FROM");
+	}
+
 	/* Complete "GRANT/REVOKE * ON FOREIGN DATA WRAPPER *" with TO/FROM */
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny))
diff --git a/src/common/Makefile b/src/common/Makefile
index 113029bf7b..73dce1150e 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			return "AEAD_AES_128_CBC_HMAC_SHA_256";
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			return "AEAD_AES_192_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			return "AEAD_AES_256_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			return "AEAD_AES_256_CBC_HMAC_SHA_512";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index 41bd58ebdf..3695d3285b 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -4,6 +4,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 747ecb800d..d199033651 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,6 +20,8 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index ffd5e9dc82..eb59e73c0a 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -92,6 +92,9 @@ typedef enum ObjectClass
 	OCLASS_TYPE,				/* pg_type */
 	OCLASS_CAST,				/* pg_cast */
 	OCLASS_COLLATION,			/* pg_collation */
+	OCLASS_CEK,					/* pg_colenckey */
+	OCLASS_CEKDATA,				/* pg_colenckeydata */
+	OCLASS_CMK,					/* pg_colmasterkey */
 	OCLASS_CONSTRAINT,			/* pg_constraint */
 	OCLASS_CONVERSION,			/* pg_conversion */
 	OCLASS_DEFAULT,				/* pg_attrdef */
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index d01ab504b6..758696b539 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 3179be09d3..9e2c5256f7 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,9 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_colmasterkey.h',
+  'pg_colenckey.h',
+  'pg_colenckeydata.h',
 ]
 
 bki_data = [
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index f64a0ec26b..d0b9e8458d 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -115,6 +115,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c4d6adcd3e..c58b79e3a7 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5b950129de..0e9e85ebf3 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index b561e17781..7910175a6a 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,17 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	bool		attislocal BKI_DEFAULT(t);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey);
+
+	/*
+	 * User-visible type and typmod, currently used for encrypted columns.
+	 * These are only set to nondefault values if they are different from
+	 * atttypid and attypmod.
+	 */
+	Oid			attusertypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+	int32		attusertypmod BKI_DEFAULT(-1);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..c57fa18a27
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+/* ----------------
+ *		pg_colenckey definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckey
+ * ----------------
+ */
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+	Oid			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		cekacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_TOAST(pg_colenckey, 8263, 8264);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_nsp_index, 8242, ColumnEncKeyNameNspIndexId, on pg_colenckey using btree(cekname name_ops, ceknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkeydata.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+/* ----------------
+ *		pg_colenckeydata definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckeydata
+ * ----------------
+ */
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..d3bfd36279
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+	aclitem		cmkacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, on pg_colmasterkey using btree(cmkname name_ops, cmknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c867d99563..ff06d52fd0 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index b2cdea66c4..114279fa64 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 91587b99d0..c21052a3f7 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e2a7642a2b..0d6e23c9c9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6355,6 +6355,14 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -11935,4 +11943,37 @@
   proname => 'any_value_transfn', prorettype => 'anyelement',
   proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8245',
+  proname => 'pg_encrypted_det_eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'pg_encrypted_det_ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 92bcaf2c73..85e4b5554e 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,16 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 519e570c8c..3c7ab2a8fe 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -294,6 +294,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPCATEGORY_USER		'U'
 #define  TYPCATEGORY_BITSTRING	'V' /* er ... "varbit"? */
 #define  TYPCATEGORY_UNKNOWN	'X'
+#define  TYPCATEGORY_ENCRYPTED	'Y'
 #define  TYPCATEGORY_INTERNAL	'Z'
 
 #define  TYPALIGN_CHAR			'c' /* char alignment (i.e. unaligned) */
diff --git a/src/include/commands/colenccmds.h b/src/include/commands/colenccmds.h
new file mode 100644
index 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/colenccmds.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COLENCCMDS_H
+#define COLENCCMDS_H
+
+#include "catalog/objectaddress.h"
+#include "parser/parse_node.h"
+
+extern ObjectAddress CreateCEK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt);
+extern ObjectAddress CreateCMK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e7c2b91a58..11f88c59ce 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,4 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml>.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index ac6407e9f6..39a286e58a 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -164,6 +164,7 @@ typedef struct Port
 	 */
 	char	   *database_name;
 	char	   *user_name;
+	bool		column_encryption_enabled;
 	char	   *cmdline_options;
 	List	   *guc_options;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f7d7f10f7d..4e2256f2c5 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -88,8 +88,7 @@ typedef uint64 AclMode;			/* a bitmask of privilege bits */
 #define ACL_REFERENCES	(1<<5)
 #define ACL_TRIGGER		(1<<6)
 #define ACL_EXECUTE		(1<<7)	/* for functions */
-#define ACL_USAGE		(1<<8)	/* for languages, namespaces, FDWs, and
-								 * servers */
+#define ACL_USAGE		(1<<8)	/* for various object types */
 #define ACL_CREATE		(1<<9)	/* for namespaces and databases */
 #define ACL_CREATE_TEMP (1<<10) /* for databases */
 #define ACL_CONNECT		(1<<11) /* for databases */
@@ -722,6 +721,7 @@ typedef struct ColumnDef
 	char	   *colname;		/* name of column */
 	TypeName   *typeName;		/* type of column */
 	char	   *compression;	/* compression method for column */
+	List	   *encryption;		/* encryption info for column */
 	int			inhcount;		/* number of times column is inherited */
 	bool		is_local;		/* column has local (non-inherited) def'n */
 	bool		is_not_null;	/* NOT NULL constraint specified? */
@@ -758,11 +758,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1980,6 +1981,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2167,6 +2171,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..c8cf9c7776 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -149,6 +149,7 @@ PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("encryption", ENCRYPTION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -250,6 +251,7 @@ PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("master", MASTER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index d4865e50f6..26e1cd617a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..c1f6fe1410 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -29,6 +29,8 @@ PG_CMDTAG(CMDTAG_ALTER_ACCESS_METHOD, "ALTER ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_AGGREGATE, "ALTER AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CAST, "ALTER CAST", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_COLLATION, "ALTER COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY, "ALTER COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_MASTER_KEY, "ALTER COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONSTRAINT, "ALTER CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONVERSION, "ALTER CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_DATABASE, "ALTER DATABASE", false, false, false)
@@ -86,6 +88,8 @@ PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, fals
 PG_CMDTAG(CMDTAG_CREATE_AGGREGATE, "CREATE AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CAST, "CREATE CAST", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_COLLATION, "CREATE COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY, "CREATE COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_MASTER_KEY, "CREATE COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONSTRAINT, "CREATE CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONVERSION, "CREATE CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_DATABASE, "CREATE DATABASE", false, false, false)
@@ -138,6 +142,8 @@ PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CAST, "DROP CAST", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_COLLATION, "DROP COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_ENCRYPTION_KEY, "DROP COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_MASTER_KEY, "DROP COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONSTRAINT, "DROP CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONVERSION, "DROP CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_DATABASE, "DROP DATABASE", false, false, false)
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2..0c73022833 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -159,6 +159,8 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
+#define ACL_ALL_RIGHTS_CEK			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_CMK			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..1dac24e4b4 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -163,6 +163,7 @@ extern bool type_is_rowtype(Oid typid);
 extern bool type_is_enum(Oid typid);
 extern bool type_is_range(Oid typid);
 extern bool type_is_multirange(Oid typid);
+extern bool type_is_encrypted(Oid typid);
 extern void get_type_category_preferred(Oid typid,
 										char *typcategory,
 										bool *typispreferred);
@@ -202,6 +203,9 @@ extern Oid	get_publication_oid(const char *pubname, bool missing_ok);
 extern char *get_publication_name(Oid pubid, bool missing_ok);
 extern Oid	get_subscription_oid(const char *subname, bool missing_ok);
 extern char *get_subscription_name(Oid subid, bool missing_ok);
+extern char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok);
+extern char *get_cmk_name(Oid cmkid, bool missing_ok);
 
 #define type_is_array(typid)  (get_element_type(typid) != InvalidOid)
 /* type_is_array_domain accepts both plain arrays and domains over arrays */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index a443181d41..8ecacb29df 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d5d50ceab4..bf1d527c1a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAMENSP,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAMENSP,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c18e914228..10dfda8016 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,7 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 50b5df3490..bf6149e859 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -341,6 +341,14 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	{"cmklookup", "PGCMKLOOKUP", "", NULL,
+		"CMK-Lookup", "", 64,
+	offsetof(struct pg_conn, cmklookup)},
+
+	{"column_encryption", "PGCOLUMNENCRYPTION", "0", NULL,
+		"Column-Encryption", "", 1,
+	offsetof(struct pg_conn, column_encryption_setting)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -1412,6 +1420,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4029,6 +4059,22 @@ freePGconn(PGconn *conn)
 	free(conn->krbsrvname);
 	free(conn->gsslib);
 	free(conn->connip);
+	free(conn->cmklookup);
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		free(conn->cmks[i].cmkname);
+		free(conn->cmks[i].cmkrealm);
+	}
+	free(conn->cmks);
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		if (conn->ceks[i].cekdata)
+		{
+			explicit_bzero(conn->ceks[i].cekdata, conn->ceks[i].cekdatalen);
+			free(conn->ceks[i].cekdata);
+		}
+	}
+	free(conn->ceks);
 	/* Note that conn->Pfdebug is not ours to close or free */
 	free(conn->write_err_msg);
 	free(conn->inBuffer);
diff --git a/src/interfaces/libpq/fe-encrypt-openssl.c b/src/interfaces/libpq/fe-encrypt-openssl.c
new file mode 100644
index 0000000000..ab6c70967f
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,842 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt-openssl.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe-encrypt.h"
+#include "libpq-int.h"
+
+#include "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = NULL;
+	EVP_PKEY_CTX *ctx = NULL;
+	unsigned char *out = NULL;
+	size_t		outlen;
+
+	switch (cmkalg)
+	{
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			md = EVP_sha1();
+			break;
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		free(out);
+		out = NULL;
+		goto fail;
+	}
+
+	*tolen = outlen;
+
+fail:
+	EVP_PKEY_CTX_free(ctx);
+	EVP_PKEY_free(key);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05>
+ * for encrypting and decrypting column values.
+ */
+
+#ifdef TEST_ENCRYPT
+
+/*
+ * Test data from
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5>
+ */
+
+/*
+ * The different test cases just use different prefixes of K, so one constant
+ * is enough here.
+ */
+static const unsigned char K[] = {
+	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+	0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+	0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+	0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+};
+
+static const unsigned char P[] = {
+	0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20,
+	0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75,
+	0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65,
+	0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62,
+	0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69,
+	0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66,
+	0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f,
+	0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65,
+};
+
+static const unsigned char test_IV[] = {
+	0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04,
+};
+
+static const unsigned char test_A[] = {
+	0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63,
+	0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20,
+	0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+static const EVP_CIPHER *
+pg_cekalg_to_openssl_cipher(int cekalg)
+{
+	const EVP_CIPHER *cipher;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			cipher = EVP_aes_128_cbc();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			cipher = EVP_aes_192_cbc();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			cipher = EVP_aes_256_cbc();
+			break;
+		default:
+			cipher = NULL;
+	}
+
+	return cipher;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+static const EVP_MD *
+pg_cekalg_to_openssl_md(int cekalg)
+{
+	const EVP_MD *md;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			md = EVP_sha384();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			md = EVP_sha512();
+			break;
+		default:
+			md = NULL;
+	}
+
+	return md;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+static int
+md_key_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 16;
+	else if (md == EVP_sha384())
+		return 24;
+	else if (md == EVP_sha512())
+		return 32;
+	else
+		return -1;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+static int
+md_hash_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 32;
+	else if (md == EVP_sha384())
+		return 48;
+	else if (md == EVP_sha512())
+		return 64;
+	else
+		return -1;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+static bool
+get_message_auth_tag(const EVP_MD *md,
+					 const unsigned char *mac_key, int mac_key_len,
+					 const unsigned char *encr, int encrlen,
+					 unsigned char *md_value, size_t *md_len_p,
+					 const char **errmsgp)
+{
+	static char msgbuf[1024];
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	int64		al;
+	bool		result = false;
+
+	if (encrlen < 0)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("encrypted value has invalid length"));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, mac_key, mac_key_len);
+	if (!pkey)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("could not allocate key for HMAC: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, buf, bufsize))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, md_len_p))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	Assert(*md_len_p == md_hash_length(md));
+
+	/* truncate output to half the length, per spec */
+	*md_len_p /= 2;
+
+	result = true;
+fail:
+	free(buf);
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+
+/*
+ * Decrypt a column value
+ */
+unsigned char *
+decrypt_value(PGresult *res, const PGCEK *cek, int cekalg, const unsigned char *input, int inputlen, const char **errmsgp)
+{
+	static char msgbuf[1024];
+
+	const unsigned char *iv = NULL;
+	size_t		ivlen;
+
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *decr;
+	int			decrlen,
+				decrlen2;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized encryption algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized digest algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("column encryption key has wrong length for algorithm (has: %zu, required: %d)"),
+				 cek->cekdatalen, key_len);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_memcmp(input + (inputlen - md_len), md_value, md_len) != 0)
+	{
+		*errmsgp = libpq_gettext("MAC mismatch");
+		goto fail;
+	}
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	iv = input;
+	input += ivlen;
+	inputlen -= ivlen;
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = inputlen + EVP_CIPHER_CTX_block_size(evp_cipher_ctx) + 1;
+#ifndef TEST_ENCRYPT
+	buf = pqResultAlloc(res, bufsize, false);
+#else
+	buf = malloc(bufsize);
+#endif
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	decr = buf;
+	if (!EVP_DecryptUpdate(evp_cipher_ctx, decr, &decrlen, input, inputlen - md_len))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	if (!EVP_DecryptFinal_ex(evp_cipher_ctx, decr + decrlen, &decrlen2))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	decrlen += decrlen2;
+	Assert(decrlen < bufsize);
+	decr[decrlen] = '\0';
+	result = decr;
+
+fail:
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	return result;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#ifndef TEST_ENCRYPT
+static bool
+make_siv(PGconn *conn,
+		 unsigned char *iv, size_t ivlen,
+		 const EVP_MD *md,
+		 const unsigned char *iv_key, int iv_key_len,
+		 const unsigned char *plaintext, int plaintext_len)
+{
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	bool		result = false;
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, iv_key, iv_key_len);
+	if (!pkey)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	Assert(md_len == md_hash_length(md));
+	memcpy(iv, md_value, ivlen);
+
+	result = true;
+fail:
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+#endif							/* TEST_ENCRYPT */
+
+/*
+ * Encrypt a column value
+ */
+unsigned char *
+encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg, const unsigned char *value, int *nbytesp, bool enc_det)
+{
+	int			nbytes = *nbytesp;
+	unsigned char iv[EVP_MAX_IV_LENGTH];
+	size_t		ivlen;
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_key;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *encr;
+	int			encrlen,
+				encrlen2;
+
+	const char *errmsg;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		buf2size;
+	unsigned char *buf2 = NULL;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	Assert(ivlen <= sizeof(iv));
+	if (enc_det)
+	{
+#ifndef TEST_ENCRYPT
+		make_siv(conn, iv, ivlen, md, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	bufsize = ivlen + (nbytes + 2 * EVP_CIPHER_CTX_block_size(evp_cipher_ctx) - 1);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	encrlen += encrlen2;
+
+	encr -= ivlen;
+	encrlen += ivlen;
+
+	Assert(encrlen <= bufsize);
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  encr, encrlen,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf2, encr, encrlen);
+	memcpy(buf2 + encrlen, md_value, md_len);
+
+	result = buf2;
+	nbytes = buf2size;
+
+fail:
+	free(buf);
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	*nbytesp = nbytes;
+	return result;
+}
+
+
+/*
+ * Run test cases
+ */
+#ifdef TEST_ENCRYPT
+
+static void
+debug_print_hex(const char *name, const unsigned char *val, int len)
+{
+	printf("%s =", name);
+	for (int i = 0; i < len; i++)
+	{
+		if (i % 16 == 0)
+			printf("\n");
+		else
+			printf(" ");
+		printf("%02x", val[i]);
+	}
+	printf("\n");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, K_len);
+
+	C = encrypt_value(NULL, &cek, alg, P, &nbytes, true);
+	debug_print_hex("C", C, nbytes);
+}
+
+int
+main(int argc, char **argv)
+{
+	printf("5.1\n");
+	test_case(PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256, K, 32, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FE_ENCRYPT_H
+#define FE_ENCRYPT_H
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+extern unsigned char *decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+											int fromlen, const unsigned char *from,
+											int *tolen);
+
+extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg,
+									const unsigned char *input, int inputlen,
+									const char **errmsgp);
+
+extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg,
+									const unsigned char *value, int *nbytesp, bool enc_det);
+
+#endif							/* FE_ENCRYPT_H */
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index ec62550e38..7c7e2bac12 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,414 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+int
+pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+					  const char *keyrealm)
+{
+	char	   *keyname_copy;
+	char	   *keyrealm_copy;
+	bool		found;
+
+	keyname_copy = strdup(keyname);
+	if (!keyname_copy)
+		return EOF;
+	keyrealm_copy = strdup(keyrealm);
+	if (!keyrealm_copy)
+	{
+		free(keyname_copy);
+		return EOF;
+	}
+
+	found = false;
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		struct pg_cmk *checkcmk = &conn->cmks[i];
+
+		/* replace existing? */
+		if (checkcmk->cmkid == keyid)
+		{
+			free(checkcmk->cmkname);
+			free(checkcmk->cmkrealm);
+			checkcmk->cmkname = keyname_copy;
+			checkcmk->cmkrealm = keyrealm_copy;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newncmks;
+		struct pg_cmk *newcmks;
+		struct pg_cmk *newcmk;
+
+		newncmks = conn->ncmks + 1;
+		if (newncmks <= 0)
+			return EOF;
+		newcmks = realloc(conn->cmks, newncmks * sizeof(struct pg_cmk));
+		if (!newcmks)
+		{
+			free(keyname_copy);
+			free(keyrealm_copy);
+			return EOF;
+		}
+
+		newcmk = &newcmks[newncmks - 1];
+		newcmk->cmkid = keyid;
+		newcmk->cmkname = keyname_copy;
+		newcmk->cmkrealm = keyrealm_copy;
+
+		conn->ncmks = newncmks;
+		conn->cmks = newcmks;
+	}
+
+	return 0;
+}
+
+/*
+ * Replace placeholders in input string.  Return value malloc'ed.
+ */
+static char *
+replace_cmk_placeholders(const char *in, const char *cmkname, const char *cmkrealm, int cmkalg, const char *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					p++;
+					break;
+				case 'r':
+					appendPQExpBufferStr(&buf, cmkrealm);
+					p++;
+					break;
+				default:
+					appendPQExpBufferChar(&buf, p[0]);
+			}
+		}
+		else
+			appendPQExpBufferChar(&buf, p[0]);
+	}
+
+	return buf.data;
+}
+
+#ifndef USE_SSL
+/*
+ * Dummy implementation for non-SSL builds
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	return NULL;
+}
+#endif
+
+/*
+ * Decrypt a CEK using the given CMK.  The ciphertext is passed in
+ * "from" and "fromlen".  Return the decrypted value in a malloc'ed area, its
+ * length via "tolen".  Return NULL on error; add error messages directly to
+ * "conn".
+ */
+static unsigned char *
+decrypt_cek(PGconn *conn, const PGCMK *cmk, int cmkalg,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	char	   *cmklookup;
+	bool		found = false;
+	unsigned char *result = NULL;
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		return NULL;
+	}
+
+	/*
+	 * Analyze semicolon-separated list
+	 */
+	for (char *s = strtok(cmklookup, ";"); s; s = strtok(NULL, ";"))
+	{
+		char	   *sep;
+
+		/* split found token at '=' */
+		sep = strchr(s, '=');
+		if (!sep)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', s);
+			break;
+		}
+
+		/* matching realm? */
+		if (strncmp(s, "*", sep - s) == 0 || strncmp(s, cmk->cmkrealm, sep - s) == 0)
+		{
+			char	   *sep2;
+
+			found = true;
+
+			sep2 = strchr(sep, ':');
+			if (!sep2)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, "r");
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+int
+pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, const unsigned char *value, int len)
+{
+	PGCMK	   *cmk = NULL;
+	unsigned char *plainval = NULL;
+	int			plainvallen = 0;
+	bool		found;
+
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		if (conn->cmks[i].cmkid == cmkid)
+		{
+			cmk = &conn->cmks[i];
+			break;
+		}
+	}
+
+	if (!cmk)
+		return EOF;
+
+	plainval = decrypt_cek(conn, cmk, cmkalg, len, value, &plainvallen);
+	if (!plainval)
+		return EOF;
+
+	found = false;
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		struct pg_cek *checkcek = &conn->ceks[i];
+
+		/* replace existing? */
+		if (checkcek->cekid == keyid)
+		{
+			free(checkcek->cekdata);
+			checkcek->cekdata = plainval;
+			checkcek->cekdatalen = plainvallen;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newnceks;
+		struct pg_cek *newceks;
+		struct pg_cek *newcek;
+
+		newnceks = conn->nceks + 1;
+		if (newnceks <= 0)
+		{
+			free(plainval);
+			return EOF;
+		}
+		newceks = realloc(conn->ceks, newnceks * sizeof(struct pg_cek));
+		if (!newceks)
+		{
+			free(plainval);
+			return EOF;
+		}
+
+		newcek = &newceks[newnceks - 1];
+		newcek->cekid = keyid;
+		newcek->cekdata = plainval;
+		newcek->cekdatalen = plainvallen;
+
+		conn->nceks = newnceks;
+		conn->ceks = newceks;
+	}
+
+	return 0;
+}
 
 /*
  * pqRowProcessor
@@ -1251,13 +1662,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == res->attDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					*errmsgp = libpq_gettext("column encryption key not found");
+					goto fail;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				if (val == NULL)
+					goto fail;
+#else
+				*errmsgp = libpq_gettext("column encryption not supported by this build");
 				goto fail;
+#endif
+			}
+			else
+			{
+				val = (char *) pqResultAlloc(res, clen + 1, isbinary);
+				if (val == NULL)
+					goto fail;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* copy and zero-terminate the data (even if it's binary) */
+				memcpy(val, columns[i].value, clen);
+				val[clen] = '\0';
+			}
 
 			tup[i].len = clen;
 			tup[i].value = val;
@@ -1500,6 +1949,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1967,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2006,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2122,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2165,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2264,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2313,47 @@ PQsendQueryGuts(PGconn *conn,
 		goto sendFailed;
 
 	/* Send parameter formats */
-	if (nParams > 0 && paramFormats)
+	if (nParams > 0 && (paramFormats || (paramDesc && paramDesc->paramDescs)))
 	{
 		if (pqPutInt(nParams, 2, conn) < 0)
 			goto sendFailed;
+
 		for (i = 0; i < nParams; i++)
 		{
-			if (pqPutInt(paramFormats[i], 2, conn) < 0)
+			int			format = paramFormats ? paramFormats[i] : 0;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2372,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2390,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				bool		enc_det = (paramDesc->paramDescs[i].flags & 0x01) != 0;
+				PGCEK	   *cek = NULL;
+				char	   *enc_paramValue;
+				int			enc_nbytes = nbytes;
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == paramDesc->paramDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					libpq_append_conn_error(conn, "column encryption key not found");
+					goto sendFailed;
+				}
+
+				enc_paramValue = (char *) encrypt_value(conn, cek, paramDesc->paramDescs[i].cekalg,
+														(const unsigned char *) paramValue, &enc_nbytes, enc_det);
+				if (!enc_paramValue)
+					goto sendFailed;
+
+				if (pqPutInt(enc_nbytes, 4, conn) < 0 ||
+					pqPutnchar(enc_paramValue, enc_nbytes, conn) < 0)
+					goto sendFailed;
+
+				free(enc_paramValue);
+#else
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2872,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4140,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4188,17 @@ PQfmod(const PGresult *res, int field_num)
 		return 0;
 }
 
+int
+PQfisencrypted(const PGresult *res, int field_num)
+{
+	if (!check_field_number(res, field_num))
+		return false;
+	if (res->attDescs)
+		return (res->attDescs[field_num].cekid != 0);
+	else
+		return false;
+}
+
 char *
 PQcmdStatus(PGresult *res)
 {
@@ -3762,6 +4384,17 @@ PQparamtype(const PGresult *res, int param_num)
 		return InvalidOid;
 }
 
+int
+PQparamisencrypted(const PGresult *res, int param_num)
+{
+	if (!check_param_number(res, param_num))
+		return false;
+	if (res->paramDescs)
+		return (res->paramDescs[param_num].cekid != 0);
+	else
+		return false;
+}
+
 
 /* PQsetnonblocking:
  *	sets the PGconn's database connection non-blocking if the arg is true
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 8ab6a88416..3e3d8674be 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -43,6 +43,8 @@ static int	getRowDescriptions(PGconn *conn, int msgLength);
 static int	getParamDescriptions(PGconn *conn, int msgLength);
 static int	getAnotherTuple(PGconn *conn, int msgLength);
 static int	getParameterStatus(PGconn *conn);
+static int	getColumnMasterKey(PGconn *conn);
+static int	getColumnEncryptionKey(PGconn *conn);
 static int	getNotify(PGconn *conn);
 static int	getCopyStart(PGconn *conn, ExecStatusType copytype);
 static int	getReadyForQuery(PGconn *conn);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -547,6 +555,8 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		int			typlen;
 		int			atttypmod;
 		int			format;
+		int			cekid;
+		int			cekalg;
 
 		if (pqGets(&conn->workBuffer, conn) ||
 			pqGetInt(&tableid, 4, conn) ||
@@ -561,6 +571,21 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -582,8 +607,10 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		result->attDescs[i].typid = typid;
 		result->attDescs[i].typlen = typlen;
 		result->attDescs[i].atttypmod = atttypmod;
+		result->attDescs[i].cekid = cekid;
+		result->attDescs[i].cekalg = cekalg;
 
-		if (format != 1)
+		if ((format & 0x0F) != 1)
 			result->binary = 0;
 	}
 
@@ -685,10 +712,31 @@ getParamDescriptions(PGconn *conn, int msgLength)
 	for (i = 0; i < nparams; i++)
 	{
 		int			typid;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGetInt(&typid, 4, conn))
 			goto not_enough_data;
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&cekalg, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&flags, 2, conn))
+				goto not_enough_data;
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
 		result->paramDescs[i].typid = typid;
+		result->paramDescs[i].cekid = cekid;
+		result->paramDescs[i].cekalg = cekalg;
+		result->paramDescs[i].flags = flags;
 	}
 
 	/* Success! */
@@ -1468,6 +1516,92 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+/*
+ * Attempt to read a ColumnMasterKey message.
+ * Entry: 'y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	int			keyid;
+	char	   *keyname;
+	char	   *keyrealm;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	if (!keyname)
+		return EOF;
+	/* Get the key realm */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyrealm = strdup(conn->workBuffer.data);
+	if (!keyrealm)
+		return EOF;
+	/* And save it */
+	ret = pqSaveColumnMasterKey(conn, keyid, keyname, keyrealm);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(keyname);
+	free(keyrealm);
+
+	return ret;
+}
+
+/*
+ * Attempt to read a ColumnEncryptionKey message.
+ * Entry: 'Y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	int			keyid;
+	int			cmkid;
+	int			cmkalg;
+	char	   *buf;
+	int			vallen;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK ID */
+	if (pqGetInt(&cmkid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK algorithm */
+	if (pqGetInt(&cmkalg, 4, conn) != 0)
+		return EOF;
+	/* Get the key data len */
+	if (pqGetInt(&vallen, 4, conn) != 0)
+		return EOF;
+	/* Get the key data */
+	buf = malloc(vallen);
+	if (!buf)
+		return EOF;
+	if (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2420,9 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	if (conn->column_encryption_enabled)
+		ADD_STARTUP_OPTION("_pq_.column_encryption", "1");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index abaab6a073..77791e8349 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -450,7 +450,8 @@ pqTraceOutputS(FILE *f, const char *message, int *cursor)
 
 /* ParameterDescription */
 static void
-pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -458,12 +459,21 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 	nfields = pqTraceOutputInt16(f, message, cursor);
 
 	for (int i = 0; i < nfields; i++)
+	{
 		pqTraceOutputInt32(f, message, cursor, regress);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+			pqTraceOutputInt16(f, message, cursor);
+		}
+	}
 }
 
 /* RowDescription */
 static void
-pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -479,6 +489,11 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
 		pqTraceOutputInt16(f, message, cursor);
 		pqTraceOutputInt32(f, message, cursor, false);
 		pqTraceOutputInt16(f, message, cursor);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+		}
 	}
 }
 
@@ -514,6 +529,30 @@ pqTraceOutputW(FILE *f, const char *message, int *cursor, int length)
 		pqTraceOutputInt16(f, message, cursor);
 }
 
+/* ColumnMasterKey */
+static void
+pqTraceOutputy(FILE *f, const char *message, int *cursor, bool regress)
+{
+	fprintf(f, "ColumnMasterKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+}
+
+/* ColumnEncryptionKey */
+static void
+pqTraceOutputY(FILE *f, const char *message, int *cursor, bool regress)
+{
+	int			len;
+
+	fprintf(f, "ColumnEncryptionKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, false);
+	len = pqTraceOutputInt32(f, message, cursor, false);
+	pqTraceOutputNchar(f, len, message, cursor);
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -647,10 +686,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 				fprintf(conn->Pfdebug, "Sync"); /* no message content */
 			break;
 		case 't':				/* Parameter Description */
-			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'T':				/* Row Description */
-			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'v':				/* Negotiate Protocol Version */
 			pqTraceOutputv(conn->Pfdebug, message, &logCursor);
@@ -665,6 +706,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			fprintf(conn->Pfdebug, "Terminate");
 			/* No message content */
 			break;
+		case 'y':
+			pqTraceOutputy(conn->Pfdebug, message, &logCursor, regress);
+			break;
+		case 'Y':
+			pqTraceOutputY(conn->Pfdebug, message, &logCursor, regress);
+			break;
 		case 'Z':				/* Ready For Query */
 			pqTraceOutputZ(conn->Pfdebug, message, &logCursor);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f3d9220496..cf339a3e53 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -267,6 +267,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -438,6 +440,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,7 @@ extern int	PQfformat(const PGresult *res, int field_num);
 extern Oid	PQftype(const PGresult *res, int field_num);
 extern int	PQfsize(const PGresult *res, int field_num);
 extern int	PQfmod(const PGresult *res, int field_num);
+extern int	PQfisencrypted(const PGresult *res, int field_num);
 extern char *PQcmdStatus(PGresult *res);
 extern char *PQoidStatus(const PGresult *res);	/* old and ugly */
 extern Oid	PQoidValue(const PGresult *res);	/* new and improved */
@@ -540,6 +559,7 @@ extern int	PQgetlength(const PGresult *res, int tup_num, int field_num);
 extern int	PQgetisnull(const PGresult *res, int tup_num, int field_num);
 extern int	PQnparams(const PGresult *res);
 extern Oid	PQparamtype(const PGresult *res, int param_num);
+extern int	PQparamisencrypted(const PGresult *res, int param_num);
 
 /* Describe prepared statements and portals */
 extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d7ec5ed429..cbfaa7f95f 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -112,6 +112,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -343,6 +346,26 @@ typedef struct pg_conn_host
 								 * found in password file. */
 } pg_conn_host;
 
+/*
+ * Column encryption support data
+ */
+
+/* column master key */
+typedef struct pg_cmk
+{
+	Oid			cmkid;
+	char	   *cmkname;
+	char	   *cmkrealm;
+} PGCMK;
+
+/* column encryption key */
+typedef struct pg_cek
+{
+	Oid			cekid;
+	unsigned char *cekdata;		/* (decrypted) */
+	size_t		cekdatalen;
+} PGCEK;
+
 /*
  * PGconn stores all the state data associated with a single connection
  * to a backend.
@@ -396,6 +419,8 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *cmklookup;		/* CMK lookup specification */
+	char	   *column_encryption_setting;	/* column_encryption connection parameter (0 or 1) */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -477,6 +502,13 @@ struct pg_conn
 	PGVerbosity verbosity;		/* error/notice message verbosity */
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
+	bool		column_encryption_enabled;	/* parsed version of column_encryption_setting */
+
+	/* Column encryption support data */
+	int			ncmks;
+	PGCMK	   *cmks;
+	int			nceks;
+	PGCEK	   *ceks;
 
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
@@ -673,6 +705,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern int	pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+								  const char *keyrealm);
+extern int	pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg,
+									  const unsigned char *value, int len);
 extern int	pqRowProcessor(PGconn *conn, const char **errmsgp);
 extern void pqCommandQueueAdvance(PGconn *conn);
 extern int	PQsendQueryContinue(PGconn *conn, const char *query);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 573fd9b6ea..a6e6b2ada9 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,7 @@ endif
 
 if ssl.found()
   libpq_sources += files('fe-secure-common.c')
+  libpq_sources += files('fe-encrypt-openssl.c')
   libpq_sources += files('fe-secure-openssl.c')
 endif
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': get_option('ssl')},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 4df544ecef..0c36aa5f32 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
+GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-encrypt-openssl.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
 GETTEXT_TRIGGERS = libpq_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext pqInternalNotice:2
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 100644
--- a/src/interfaces/libpq/test/Makefile
+++ b/src/interfaces/libpq/test/Makefile
@@ -15,10 +15,17 @@ override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
 LDFLAGS_INTERNAL += $(libpq_pgport)
 
 PROGS = libpq_testclient libpq_uri_regress
+ifeq ($(with_ssl),openssl)
+PROGS += libpq_test_encrypt
+endif
+
 
 all: $(PROGS)
 
 $(PROGS): $(WIN32RES)
 
+libpq_test_encrypt: ../fe-encrypt-openssl.c
+	$(CC) $(CPPFLAGS) -DTEST_ENCRYPT $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 clean distclean maintainer-clean:
 	rm -f $(PROGS) *.o
diff --git a/src/interfaces/libpq/test/meson.build b/src/interfaces/libpq/test/meson.build
index b2a4b06fd2..87d2808b52 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c8ba170503 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -24,7 +24,7 @@ ifeq ($(with_ldap),yes)
 SUBDIRS += ldap
 endif
 ifeq ($(with_ssl),openssl)
-SUBDIRS += ssl
+SUBDIRS += column_encryption ssl
 endif
 
 # Test suites that are not safe by default but can be run if selected
@@ -36,7 +36,7 @@ export PG_TEST_EXTRA
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl column_encryption)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..d5ead874e5
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/column_encryption/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+export OPENSSL PERL
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/meson.build b/src/test/column_encryption/meson.build
new file mode 100644
index 0000000000..47f88c41ce
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,24 @@
+column_encryption_test_client = executable('test_client',
+  files('test_client.c'),
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+testprep_targets += column_encryption_test_client
+
+tests += {
+  'name': 'column_encryption',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_column_encryption.pl',
+      't/002_cmk_rotation.pl',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+      'PERL': perl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..c56af737ac
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,255 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+my $perl = $ENV{PERL};
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$openssl, 'pkeyutl', '-encrypt',
+		'-inkey', $cmkfilename,
+		'-pkeyopt', 'rsa_padding_mode:oaep',
+		'-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+		'-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+	# create CEK in database
+	$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = ${cmkname}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c smallint ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# Expected ciphertext length is 2 blocks of AES output (2 * 16) plus
+# half SHA-256 output (16) in hex encoding: (2 * 16 + 16) * 2 = 96
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1) TO STDOUT}),
+	qr/1\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text
+c|smallint),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result');
+
+{
+	local $ENV{PGCMKLOOKUP} = '*=run:broken %k %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = "*=run:$perl ./test_run_decrypt.pl %k %a %p";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$node->command_fails_like(['test_client', 'test1'], qr/not encrypted/, 'test client fails because parameters not encrypted');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result after test client insert');
+
+$node->command_ok(['test_client', 'test2'], 'test client test 2');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3|33),
+	'decrypted query result after test client insert 2');
+
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1 WHERE a = 3) TO STDOUT}),
+	qr/3\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3upd|33),
+	'decrypted query result after update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+is($node->safe_psql('postgres', q{SELECT b, count(*) FROM tbl2 GROUP BY b ORDER BY 2}),
+	q(valB|1
+valA|2),
+	'group by deterministically encrypted column');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl3 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c text ENCRYPTED WITH (column_encryption_key = cek2, algorithm = 'AEAD_AES_192_CBC_HMAC_SHA_384')
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1
+2|valB2|valC2
+3|valB3|valC3),
+	'decrypted query result multiple keys after second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..14eafb8ec9
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Test column master key rotation.  First, we generate CMK1 and a CEK
+# encrypted with it.  Then we add a CMK2 and encrypt the CEK with it
+# as well.  (Recall that a CEK can be associated with multiple CMKs,
+# for this reason.  That's why pg_colenckeydata is split out from
+# pg_colenckey.)  Then we remove CMK1.  We test that we can get
+# decrypted query results at each step.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# generate random bytes
+system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+# encrypt CEK using CMK
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk1filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# create CEK in database
+$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = cmk1, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b) VALUES (1, $1) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with one CMK');
+
+
+# create new CMK
+my $cmk2filename = create_cmk('cmk2');
+
+# encrypt CEK using new CMK
+#
+# (Here, we still have the plaintext of the CEK available from
+# earlier.  In reality, one would decrypt the CEK with the first CMK
+# and then re-encrypt it with the second CMK.)
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk2filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+$cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# add new data record for CEK in database
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} ADD VALUE (column_master_key = cmk2, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with two CMKs');
+
+
+# delete CEK record for first CMK
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} DROP VALUE (column_master_key = cmk1);});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with only new CMK');
+
+
+done_testing();
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (!(!PQparamisencrypted(res2, 0) &&
+		  PQparamisencrypted(res2, 1)))
+	{
+		fprintf(stderr, "wrong results from PQparamisencrypted()\n");
+		return 1;
+	}
+
+	res = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	PGconn	   *conn;
+	int			ret = 0;
+
+	conn = PQconnectdb("");
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		fprintf(stderr, "Connection to database failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (argc < 2 || argv[1] == NULL)
+		return 87;
+	else if (strcmp(argv[1], "test1") == 0)
+		ret = test1(conn);
+	else if (strcmp(argv[1], "test2") == 0)
+		ret = test2(conn);
+	else if (strcmp(argv[1], "test3") == 0)
+		ret = test3(conn);
+	else if (strcmp(argv[1], "test4") == 0)
+		ret = test4(conn);
+	else
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+
+# Test/sample command for libpq cmklookup run scheme
+#
+# This just places the data into temporary files and runs the openssl
+# command on it.  (In practice, this could more simply be written as a
+# shell script, but this way it's more portable.)
+
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $fh, '<:raw', "${tmpdir}/output.tmp" or die $!;
+my $data = '';
+
+while (1) {
+	my $success = read $fh, $data, 100, length($data);
+	die $! if not defined $success;
+	last if not $success;
+}
+
+close $fh;
+
+unlink "${tmpdir}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..d55ddb1ab7 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,7 @@ subdir('subscription')
 subdir('modules')
 
 if ssl.found()
+  subdir('column_encryption')
   subdir('ssl')
 endif
 
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..193ccacfff
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,353 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE ROLE regress_enc_user1;
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+CREATE COLUMN MASTER KEY cmk2;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "cek1" already has data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "fail" does not exist
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+ERROR:  column encryption key "notexist" does not exist
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+\d tbl_29f3
+              Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_29f3
+                                        Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+\d tbl_447f
+              Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_447f
+                                        Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  permission denied for column master key cmk1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+ERROR:  permission denied for column encryption key cek1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column master key cmk1 depends on column master key cmk1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ERROR:  column master key "cmk3" already exists in schema "public"
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+ERROR:  column master key "cmkx" does not exist
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ERROR:  column encryption key "cek3" already exists in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+ERROR:  must be owner of column encryption key cek3
+DROP COLUMN MASTER KEY cmk3;  -- fail
+ERROR:  must be owner of column master key cmk3
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ERROR:  column encryption key "cek1" has no data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ERROR:  column master key "fail" does not exist
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+ERROR:  attribute "algorithm" must not be specified
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+ERROR:  column encryption key "fail" does not exist
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf..77fedce8aa 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -107,7 +109,8 @@ BEGIN
         ('text search template'), ('text search configuration'),
         ('policy'), ('user mapping'), ('default acl'), ('transform'),
         ('operator of access method'), ('function of access method'),
-        ('publication namespace'), ('publication relation')
+        ('publication namespace'), ('publication relation'),
+        ('column encryption key'), ('column encryption key data'), ('column master key')
     LOOP
         FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
         LOOP
@@ -327,6 +330,24 @@ WARNING:  error for publication relation,{addr_nsp,zwei},{}: argument list lengt
 WARNING:  error for publication relation,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist
 WARNING:  error for publication relation,{eins,zwei,drei},{}: argument list length must be exactly 1
 WARNING:  error for publication relation,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei"
+WARNING:  error for column encryption key,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins},{}: column master key "eins" does not exist
+WARNING:  error for column master key,{eins},{integer}: column master key "eins" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 -- these object types cannot be qualified names
 SELECT pg_get_object_address('language', '{one}', '{}');
 ERROR:  language "one" does not exist
@@ -409,6 +430,9 @@ WITH objects (type, name, args) AS (VALUES
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,8 @@ drop cascades to user mapping for regress_addr_user on server integer
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 DROP SCHEMA addr_nsp CASCADE;
 NOTICE:  drop cascades to 14 other objects
 DETAIL:  drop cascades to text search dictionary addr_ts_dict
@@ -547,6 +576,9 @@ WITH objects (classid, objid, objsubid) AS (VALUES
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|NULL
+("(""column master key"",,,)")|("(""column master key"",,)")|NULL
+("(""column encryption key"",,,)")|("(""column encryption key"",,)")|NULL
+("(""column encryption key data"",,,)")|("(""column encryption key data"",,)")|NULL
 -- restore normal output mode
 \a\t
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..226f5e404e 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attusertypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,9 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,13 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
  anyrange                    | anymultirange
-(5 rows)
+(6 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -872,6 +876,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+pg_encrypted_det_eq(pg_encrypted_det,pg_encrypted_det)
+pg_encrypted_det_ne(pg_encrypted_det,pg_encrypted_det)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -75,7 +75,9 @@ ORDER BY t1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | pg_encrypted_det
+ 8244 | pg_encrypted_rnd
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT t1.oid, t1.typname as basetype, t2.typname as arraytype,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 15e015b3d6..577a6d4b2e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 427429975e..57c7d2bec3 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..6e63e6d4f5
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,265 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE ROLE regress_enc_user1;
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk2;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+\d tbl_29f3
+\d+ tbl_29f3
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+
+\d tbl_447f
+\d+ tbl_447f
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+
+
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..35af43032f 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -99,7 +101,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
         ('text search template'), ('text search configuration'),
         ('policy'), ('user mapping'), ('default acl'), ('transform'),
         ('operator of access method'), ('function of access method'),
-        ('publication namespace'), ('publication relation')
+        ('publication namespace'), ('publication relation'),
+        ('column encryption key'), ('column encryption key data'), ('column master key')
     LOOP
         FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
         LOOP
@@ -174,6 +177,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -228,6 +234,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 
 DROP SCHEMA addr_nsp CASCADE;
 
@@ -247,6 +255,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 2ddab010c2777c6a965cea82dc1b809ddc33ecc1
-- 
2.39.2



^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* Re: Transparent column encryption
@ 2023-02-28 20:28  Peter Eisentraut <[email protected]>
  parent: Peter Eisentraut <[email protected]>
  0 siblings, 1 reply; 62+ messages in thread

From: Peter Eisentraut @ 2023-02-28 20:28 UTC (permalink / raw)
  To: pgsql-hackers

On 22.02.23 11:25, Peter Eisentraut wrote:
> Other changes since v15:
> 
> - CEKs and CMKs now have USAGE privileges.  (There are some TODO markers 
> where I got too bored with boilerplate.  I will fill those in, but the 
> idea should be clear.)

New patch.  The above is all filled in now.

I also figured we need support in the DISCARD command to clear the 
session state of what keys have already been sent, for the benefit of 
connection poolers, so I added an option there.

The only thing left on my list for this whole thing is some syntax in 
psql to force encryption for a parameter.  But that could also be done 
as a separate patch.

From 65eef95ef3a89178378d36fcd65a67c0d1592693 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Tue, 28 Feb 2023 21:23:25 +0100
Subject: [PATCH v17] Automatic client-side column-level encryption

This feature enables the automatic encryption and decryption of
particular columns in the client.  The data for those columns then
only ever appears in ciphertext on the server, so it is protected from
DBAs, sysadmins, cloud operators, etc. as well as accidental leakage
to server logs, file-system backups, etc.  The canonical use case for
this feature is storing credit card numbers encrypted, in accordance
with PCI DSS, as well as similar situations involving social security
numbers etc.  One can't do any computations with encrypted values on
the server, but for these use cases, that is not necessary.  This
feature does support deterministic encryption as an alternative to the
default randomized encryption, so in that mode one can do equality
lookups, at the cost of some security.

This functionality also exists in other database products, and the
overall concepts were mostly adopted from there.

(Note: This feature has nothing to do with any on-disk encryption
feature.  Both can exist independently.)

You declare a column as encrypted in a CREATE TABLE statement.  The
column value is encrypted by a symmetric key called the column
encryption key (CEK).  The CEK is a catalog object.  The CEK key
material is in turn encrypted by an asymmetric key called the column
master key (CMK).  The CMK is not stored in the database but somewhere
where the client can get to it, for example in a file or in a key
management system.  When a server sends rows containing encrypted
column values to the client, it first sends the required CMK and CEK
information (new protocol messages), which the client needs to record.
Then, the client can use this information to automatically decrypt the
incoming row data and forward it in plaintext to the application.

For the CMKs, libpq has a new connection parameter "cmklookup" that
specifies via a mini-language where to get the keys.  Right now, you
can use "file" to read it from a file, or "run" to run some program,
which could get it from a KMS.

The general idea would be for an application to have one CMK per area
of secret stuff, for example, for credit card data.  The CMK can be
rotated: each CEK can be represented multiple times in the database,
encrypted by a different CMK.  (The CEK can't be rotated easily, since
that would require reading out all the data from a table/column and
reencrypting it.  We could/should add some custom tooling for that,
but it wouldn't be a routine operation.)

Several encryption algorithms are provided.  The CMK process uses
RSAES_OAEP_SHA_1 or _256.  The CEK process uses
AEAD_AES_*_CBC_HMAC_SHA_* with several strengths.

In the server, the encrypted datums are stored in types called
pg_encrypted_rnd and pg_encrypted_det (for randomized and
deterministic encryption).  These are essentially cousins of bytea.
For the rest of the database system below the protocol handling, there
is nothing special about those.  For example, pg_encrypted_rnd has no
operators at all, pg_encrypted_det has only an equality operator.
pg_attribute has a new column attrealtypid that stores the original
type of the data in the column.  This is only used for providing it to
clients, so that higher-level clients can convert the decrypted value
to their appropriate data types in their environments.

The protocol extensions are guarded by a new protocol extension option
"_pq_.column_encryption".  If this is not set, nothing changes, the
protocol stays the same, and no encryption or decryption happens.

To get automatically encrypted data into the database (as opposed to
reading it out), it is required to use protocol-level prepared
statements (i.e., extended query).  The client must first prepare a
statement, then describe the statement to get parameter metadata,
which indicates which parameters are to be encrypted and how.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this if the
\bind command is used.

Another challenge is that the parse analysis must check which
underlying column a parameter corresponds to.  This is similar to
resorigtbl and resorigcol in the opposite direction.

Discussion: https://www.postgresql.org/message-id/flat/89157929-c2b6-817b-6025-8e4b2d89d88f%40enterprisedb.com
---
 doc/src/sgml/acronyms.sgml                    |  18 +
 doc/src/sgml/catalogs.sgml                    | 317 +++++++
 doc/src/sgml/charset.sgml                     |  10 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 444 +++++++++
 doc/src/sgml/func.sgml                        |  60 ++
 doc/src/sgml/glossary.sgml                    |  26 +
 doc/src/sgml/libpq.sgml                       | 322 +++++++
 doc/src/sgml/protocol.sgml                    | 467 ++++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |  10 +
 .../ref/create_column_encryption_key.sgml     | 173 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  55 +-
 doc/src/sgml/ref/discard.sgml                 |  14 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/grant.sgml                   |  12 +-
 doc/src/sgml/ref/pg_dump.sgml                 |  42 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   8 +
 src/backend/access/common/printtup.c          | 237 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  60 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 288 ++++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  17 +
 src/backend/commands/colenccmds.c             | 439 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/discard.c                |   8 +-
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 204 ++++-
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 200 ++++-
 src/backend/parser/parse_param.c              | 157 ++++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  56 ++
 src/backend/utils/adt/acl.c                   | 398 +++++++++
 src/backend/utils/adt/varlena.c               | 107 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  42 +
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/dumputils.c                   |   4 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 377 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  35 +
 src/bin/pg_dump/pg_dump_sort.c                |  14 +
 src/bin/pg_dump/pg_dumpall.c                  |   5 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  72 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 191 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  69 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   4 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |  11 +
 src/include/catalog/pg_colenckey.h            |  46 +
 src/include/catalog/pg_colenckeydata.h        |  46 +
 src/include/catalog/pg_colmasterkey.h         |  47 +
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  10 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               | 103 +++
 src/include/catalog/pg_type.dat               |  13 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  44 +-
 src/include/parser/kwlist.h                   |   3 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   7 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 839 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 671 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 157 +++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  24 +
 .../t/001_column_encryption.pl                | 255 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 451 ++++++++++
 src/test/regress/expected/object_address.out  |  37 +-
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 297 +++++++
 src/test/regress/sql/object_address.sql       |  13 +-
 src/test/regress/sql/type_sanity.sql          |   2 +
 144 files changed, 10399 insertions(+), 91 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/alter_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_master_key.sgml
 create mode 100644 src/backend/commands/colenccmds.c
 create mode 100644 src/common/colenc.c
 create mode 100644 src/include/catalog/pg_colenckey.h
 create mode 100644 src/include/catalog/pg_colenckeydata.h
 create mode 100644 src/include/catalog/pg_colmasterkey.h
 create mode 100644 src/include/commands/colenccmds.h
 create mode 100644 src/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 create mode 100644 src/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/meson.build
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/t/002_cmk_rotation.pl
 create mode 100644 src/test/column_encryption/test_client.c
 create mode 100755 src/test/column_encryption/test_run_decrypt.pl
 create mode 100644 src/test/regress/expected/column_encryption.out
 create mode 100644 src/test/regress/sql/column_encryption.sql

diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 2df6559acc..3a5f2c254c 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -56,6 +56,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CEK</acronym></term>
+    <listitem>
+     <para>
+      Column Encryption Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CIDR</acronym></term>
     <listitem>
@@ -67,6 +76,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CMK</acronym></term>
+    <listitem>
+     <para>
+      Column Master Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CPAN</acronym></term>
     <listitem>
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1e4048054..808f29669d 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -105,6 +105,21 @@ <title>System Catalogs</title>
       <entry>collations (locale information)</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link></entry>
+      <entry>column encryption keys</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link></entry>
+      <entry>column encryption key data</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link></entry>
+      <entry>column master keys</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link></entry>
       <entry>check constraints, unique constraints, primary key constraints, foreign key constraints</entry>
@@ -1360,6 +1375,44 @@ <title><structname>pg_attribute</structname> Columns</title>
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attcek</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, a reference to the column encryption key, else 0.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type of the
+       encrypted data that is reported to the client.  If the column is not
+       encrypted, then 0.  For encrypted columns, the field
+       <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.
+       </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type
+       modifier (analogous to <structfield>atttypmod</structfield>) that is
+       reported to the client.  If the column is not encrypted, then -1.  For
+       encrypted columns, the field <structfield>atttypmod</structfield>)
+       contains the identifier of the encryption algorithm; see <xref
+       linkend="protocol-cek"/> for possible values.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>attinhcount</structfield> <type>int4</type>
@@ -2467,6 +2520,270 @@ <title><structname>pg_collation</structname> Columns</title>
   </para>
  </sect1>
 
+ <sect1 id="catalog-pg-colenckey">
+  <title><structname>pg_colenckey</structname></title>
+
+  <indexterm zone="catalog-pg-colenckey">
+   <primary>pg_colenckey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckey</structname> contains information
+   about the column encryption keys in the database.  The actual key material
+   of the column encryption keys is in the catalog <link
+   linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link>.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column encryption key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colenckeydata">
+  <title><structname>pg_colenckeydata</structname></title>
+
+  <indexterm zone="catalog-pg-colenckeydata">
+   <primary>pg_colenckeydata</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckeydata</structname> contains the key
+   material of column encryption keys.  Each column encryption key object can
+   contain several versions of the key material, each encrypted with a
+   different column master key.  That allows the gradual rotation of the
+   column master keys.  Thus, <literal>(ckdcekid, ckdcmkid)</literal> is a
+   unique key of this table.
+  </para>
+
+  <para>
+   The key material of column encryption keys should never be decrypted inside
+   the database instance.  It is meant to be sent as-is to the client, where
+   it is decrypted using the associated column master key, and then used to
+   encrypt or decrypt column values.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckeydata</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcekid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column encryption key this entry belongs to
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column master key that the key material is encrypted with
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkalg</structfield> <type>int4</type>
+      </para>
+      <para>
+       The encryption algorithm used for encrypting the key material; see
+       <xref linkend="protocol-cmk"/> for possible values.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdencval</structfield> <type>bytea</type>
+      </para>
+      <para>
+       The key material of this column encryption key, encrypted using the
+       referenced column master key
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colmasterkey">
+  <title><structname>pg_colmasterkey</structname></title>
+
+  <indexterm zone="catalog-pg-colmasterkey">
+   <primary>pg_colmasterkey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colmasterkey</structname> contains information
+   about column master keys.  The keys themselves are not stored in the
+   database.  The catalog entry only contains information that is used by
+   clients to locate the keys, for example in a file or in a key management
+   system.
+  </para>
+
+  <table>
+   <title><structname>pg_colmasterkey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column master key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkrealm</structfield> <type>text</type>
+      </para>
+      <para>
+       A <quote>realm</quote> associated with this column master key.  This is
+       a freely chosen string that is used by clients to determine how to look
+       up the key.  A typical configuration would put all CMKs that are looked
+       up in the same way into the same realm.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml
index 3032392b80..f3026fff83 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,16 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When automatic client-side column-level encryption is used, then no
+     encoding conversion is possible.  (The encoding conversion happens on the
+     server, and the server cannot look inside any encrypted column values.)
+     If automatic client-side column-level encryption is enabled for a
+     session, then the server enforces that the client encoding matches the
+     server encoding, and any attempts to change the client encoding will be
+     rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..67fce16872 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <title>Pseudo-Types</title>
 
   </sect1>
 
+  <sect1 id="datatype-encrypted">
+   <title>Types Related to Encryption</title>
+
+   <para>
+    An encrypted column value (see <xref linkend="ddl-column-encryption"/>) is
+    internally stored using the types
+    <type>pg_encrypted_rnd</type> (for randomized encryption) or
+    <type>pg_encrypted_det</type> (for deterministic encryption); see <xref
+    linkend="datatype-encrypted-table"/>.  Most of the database system treats
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support automatic client-side column-level encryption
+    or have disabled it will see the encrypted values in this format.  Clients
+    that support automatic client-side column-level encryption will not see
+    these types in result sets, as the protocol layer will translate them back
+    to the declared underlying type in the table definition.
+   </para>
+
+    <table id="datatype-encrypted-table">
+     <title>Types Related to Encryption</title>
+     <tgroup cols="3">
+      <colspec colname="col1" colwidth="1*"/>
+      <colspec colname="col2" colwidth="3*"/>
+      <colspec colname="col3" colwidth="2*"/>
+      <thead>
+       <row>
+        <entry>Name</entry>
+        <entry>Storage Size</entry>
+        <entry>Description</entry>
+       </row>
+      </thead>
+      <tbody>
+       <row>
+        <entry><type>pg_encrypted_det</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, deterministic encryption</entry>
+       </row>
+       <row>
+        <entry><type>pg_encrypted_rnd</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, randomized encryption</entry>
+       </row>
+      </tbody>
+     </tgroup>
+    </table>
+  </sect1>
  </chapter>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 5179125510..65514e119f 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,440 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   With <firstterm>automatic client-side column-level encryption</firstterm>,
+   columns can be stored encrypted in the database.  The encryption and
+   decryption happens automatically on the client, so that the plaintext value
+   is never seen in the database instance or on the server hosting the
+   database.  The drawback is that most operations, such as function calls or
+   sorting, are not possible on encrypted values.
+  </para>
+
+  <sect2>
+   <title>Using Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   Automatic client-side column-level encryption uses two levels of
+   cryptographic keys.  The actual column value is encrypted using a symmetric
+   algorithm, such as AES, using a <firstterm>column encryption
+   key</firstterm> (<acronym>CEK</acronym>).  The column encryption key is in
+   turn encrypted using an asymmetric algorithm, such as RSA, using a
+   <firstterm>column master key</firstterm> (<acronym>CMK</acronym>).  The
+   encrypted CEK is stored in the database system.  The CMK is not stored in
+   the database system; it is stored on the client or somewhere where the
+   client can access it, such as in a local file or in a key management
+   system.  The database system only records where the CMK is stored and
+   provides this information to the client.  When rows containing encrypted
+   columns are sent to the client, the server first sends any necessary CMK
+   information, followed by any required CEK.  The client then looks up the
+   CMK and uses that to decrypt the CEK.  Then it decrypts incoming row data
+   using the CEK and provides the decrypted row data to the application.
+  </para>
+
+  <para>
+   Here is an example declaring a column as encrypted:
+<programlisting>
+CREATE TABLE customers (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    creditcard_num text <emphasis>ENCRYPTED WITH (column_encryption_key = cek1)</emphasis>
+);
+</programlisting>
+  </para>
+
+  <para>
+   Column encryption supports <firstterm>randomized</firstterm>
+   (also known as <firstterm>probabilistic</firstterm>) and
+   <firstterm>deterministic</firstterm> encryption.  The above example uses
+   randomized encryption, which is the default.  Randomized encryption uses a
+   random initialization vector for each encryption, so that even if the
+   plaintext of two rows is equal, the encrypted values will be different.
+   This prevents someone with direct access to the database server from making
+   computations such as distinct counts on the encrypted values.
+   Deterministic encryption uses a fixed initialization vector.  This reduces
+   security, but it allows equality searches on encrypted values.  The
+   following example declares a column with deterministic encryption:
+<programlisting>
+CREATE TABLE employees (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    ssn text ENCRYPTED WITH (
+        column_encryption_key = cek1, <emphasis>encryption_type = deterministic</emphasis>)
+);
+</programlisting>
+  </para>
+
+  <para>
+   Null values are not encrypted by automatic client-side column-level
+   encryption; null values sent by the client are visible as null values in
+   the database.  If the fact that a value is null needs to be hidden from the
+   server, this information needs to be encoded into a nonnull value in the
+   client somehow.
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Reading and Writing Encrypted Columns</title>
+
+   <para>
+    Reading and writing encrypted columns is meant to be handled automatically
+    by the client library/driver and should be mostly transparent to the
+    application code, if certain prerequisites are fulfilled:
+
+    <itemizedlist>
+     <listitem>
+      <para>
+       The client library needs to support automatic client-side column-level
+       encryption.  Not all client libraries do.  Furthermore, the client
+       library might require that automatic client-side column-level
+       encryption is explicitly enabled at connection time.  See the
+       documentation of the client library for details.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Column master keys and column encryption keys have been set up, and the
+       client library has been configured to be able to look up column master
+       keys from the key store or key management system.
+      </para>
+     </listitem>
+    </itemizedlist>
+   </para>
+
+   <para>
+    Reading from encrypted columns will then work automatically.  For example,
+    using the above example,
+<programlisting>
+SELECT ssn FROM employees WHERE id = 5;
+</programlisting>
+    would return the unencrypted value for the <literal>ssn</literal> column
+    in any rows found.
+   </para>
+
+   <para>
+    Writing to encrypted columns requires that the extended query protocol
+    (protocol-level prepared statements) be used, so that the values to be
+    encrypted are supplied separately from the SQL command.  For example,
+    using, say, psql or libpq, the following would not work:
+<programlisting>
+-- WRONG!
+INSERT INTO employees (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This would leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of client-side column-level encryption.
+    (And even ignoring that, it could not work because the server does not
+    have access to the keys to perform the encryption.)  Note that using
+    server-side prepared statements using the SQL commands
+    <command>PREPARE</command> and <command>EXECUTE</command> is equally
+    incorrect, since that would also leak the parameters provided to
+    <command>EXECUTE</command> to the server.
+   </para>
+
+   <para>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</programlisting>
+    Higher-level client libraries might use the protocol-level prepared
+    statements automatically and thus won't require any code changes.
+   </para>
+
+   <para>
+    <application>psql</application> provides the command
+    <literal>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   The steps to set up automatic client-side column-level encryption for a
+   database are:
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK, for example, using a cryptographic
+      library or toolkit, or a key management system.  Secure access to the
+      key as appropriate, using access control, passwords, etc.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database using the SQL command <xref
+      linkend="sql-create-column-master-key"/>.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the (unencrypted) key material for the CEK in a temporary
+      location.  (It will be encrypted in the next step.  Depending on the
+      available tools, it might be possible and sensible to combine these two
+      steps.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material using the CMK (created earlier).
+      (The unencrypted version of the CEK key material can now be disposed
+      of.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database using the SQL command <xref
+      linkend="sql-create-column-encryption-key"/>.  This command
+      <quote>uploads</quote> the encrypted CEK key material created in the
+      previous step to the database server.  The local copy of the CEK key
+      material can then be removed.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns using the created CEK.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the client library/driver to be able to look up the CMK
+      created earlier.
+     </para>
+    </listitem>
+   </orderedlist>
+
+   Once this is done, values can be written to and read from the encrypted
+   columns in a transparent way.
+  </para>
+
+  <para>
+   Note that these steps should not be run on the database server, but on some
+   client machine.  Neither the CMK nor the unencrypted CEK should ever appear
+   on the database server host.
+  </para>
+
+  <para>
+   The specific details of this setup depend on the desired CMK storage
+   mechanism/key management system as well as the client libraries to be used.
+   The following example uses the <command>openssl</command> command-line tool
+   to set up the keys.
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK and write it to a file:
+<programlisting>
+openssl genpkey -algorithm rsa -out cmk1.pem
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database:
+<programlisting>
+psql ... -c "CREATE COLUMN MASTER KEY cmk1"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</programlisting>
+      (See <xref linkend="protocol-cek-table"/> for required key lengths.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material:
+<programlisting>
+openssl pkeyutl -encrypt -inkey cmk1.pem -pkeyopt rsa_padding_mode:oaep -in cek1.bin -out cek1.bin.enc
+rm cek1.bin
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database:
+<programlisting>
+# convert file contents to hex encoding; this is just one possible way
+cekenchex=$(perl -0ne 'print unpack "H*"' cek1.bin.enc)
+psql ... -c "CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\x${cekenchex}')"
+rm cek1.bin.enc
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns as shown in the examples above.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the libpq for CMK lookup (see also <xref linkend="libpq-connect-cmklookup"/>):
+<programlisting>
+PGCMKLOOKUP="*=file:$PWD/%k.pem"
+export PGCMKLOOKUP
+</programlisting>
+     </para>
+
+     <para>
+      Additionally, libpq requires that the connection parameter <xref
+      linkend="libpq-connect-column-encryption"/> be set in order to activate
+      the automatic client-side column-level encryption functionality.  This
+      should be done in the connection parameters of the application, but an
+      environment variable (<envar>PGCOLUMNENCRYPTION</envar>) is also
+      available.
+     </para>
+    </listitem>
+   </orderedlist>
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Guidance on Using Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use automatic client-side column-level encryption, and what precautions
+    need to be taken to maintain its security.
+   </para>
+
+   <para>
+    In general, column encryption is never a replacement for additional
+    security and encryption techniques such as transport encryption
+    (SSL/TLS), storage encryption, strong access control, and password
+    security.  Column encryption only targets specific use cases and should be
+    used in conjunction with additional security measures.
+   </para>
+
+   <para>
+    A typical use case for column encryption is to encrypt specific values
+    with additional security requirements, for example credit card numbers.
+    This allows you to store that security-sensitive data together with the
+    rest of your data (thus getting various benefits, such as referential
+    integrity, consistent backups), while giving access to that data only to
+    specific clients and preventing accidental leakage on the server side
+    (server logs, file system backups, etc.).
+   </para>
+
+   <para>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation; for
+    <application>libpq</application>, see <xref
+    linkend="libpq-connect-column-encryption"/>).
+   </para>
+
+   <para>
+    Column encryption cannot hide the existence or absence of data, it can
+    only disguise the particular data that is known to exist.  For example,
+    storing a cleartext person name and an encrypted credit card number
+    indicates that the person has a credit card.  That might not reveal too
+    much if the database is for an online store and there is other data nearby
+    that shows that the person has recently made purchases.  But in another
+    example, storing a cleartext person name and an encrypted diagnosis in a
+    medical database probably indicates that the person has a medical issue.
+    Depending on the circumstances, that might not by itself be sufficient
+    security.
+   </para>
+
+   <para>
+    Encryption cannot completely hide the length of values.  The encryption
+    methods will pad values to multiples of the underlying cipher's block size
+    (usually 16 bytes), so some length differences will be unified this way.
+    There is no concern if all values are of the same length, but if there are
+    signficant length differences between valid values and that length
+    information is security-sensitive, then application-specific workarounds
+    such as padding would need to be applied.  How to do that securely is
+    beyond the scope of this manual.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <tip>
+    <para>
+     One might be inclined to think of the client-side column-level encryption
+     feature as a mechanism for application writers and users to protect
+     themselves against an <quote>evil DBA</quote>, but that is not the
+     intended purpose.  Rather, it is (also) a tool for the DBA to control
+     which data they do not want (in plaintext) on the server.
+    </para>
+   </tip>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on is usually
+     subject to government or industry regulations.  This section is not meant
+     to provide complete instructions on how to do this correctly.  Please
+     seek additional advice when engaging in such projects.
+    </para>
+   </note>
+  </sect2>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
@@ -1986,6 +2420,14 @@ <title>Privileges</title>
        server.  Grantees may also create, alter, or drop their own user
        mappings associated with that server.
       </para>
+      <para>
+       For column master keys, allows the creation of column encryption keys
+       using the master key.
+      </para>
+      <para>
+       For column encryption keys, allows the use of the key in the creation
+       of table columns.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2152,6 +2594,8 @@ <title>ACL Privilege Abbreviations</title>
       <entry><literal>USAGE</literal></entry>
       <entry><literal>U</literal></entry>
       <entry>
+       <literal>COLUMN ENCRYPTION KEY</literal>,
+       <literal>COLUMN MASTER KEY</literal>,
        <literal>DOMAIN</literal>,
        <literal>FOREIGN DATA WRAPPER</literal>,
        <literal>FOREIGN SERVER</literal>,
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 97b3f1c1a6..dfaa6ff6d8 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22859,6 +22859,40 @@ <title>Access Privilege Inquiry Functions</title>
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_encryption_key_privilege</primary>
+        </indexterm>
+        <function>has_column_encryption_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cek</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column encryption key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_master_key_privilege</primary>
+        </indexterm>
+        <function>has_column_master_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cmk</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column master key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
@@ -23349,6 +23383,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 7c01a541fe..818038d860 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,32 @@ <title>Glossary</title>
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-column-encryption-key">
+   <glossterm>Column encryption key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt column values when using automatic
+     client-side column-level encryption (<xref
+     linkend="ddl-column-encryption"/>).  Column encryption keys are stored in
+     the database encrypted by another key, the <glossterm
+     linkend="glossary-column-master-key">column master key</glossterm>.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt <glossterm
+     linkend="glossary-column-encryption-key">column encryption
+     keys</glossterm>.  (So the column master key is a <firstterm>key
+     encryption key</firstterm>.)  Column master keys are stored outside the
+     database system, for example in a key management system.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-commit">
    <glossterm>Commit</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3ccd8ff942..26cab10104 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,141 @@ <title>Parameter Key Words</title>
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-connect-column-encryption" xreflabel="column_encryption">
+      <term><literal>column_encryption</literal></term>
+      <listitem>
+       <para>
+        If set to <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, this enables automatic client-side column-level
+        encryption for the connection.  If encrypted columns are queried and
+        this is not enabled, the encrypted values are returned.  See <xref
+        linkend="ddl-column-encryption"/> for more information about this
+        feature.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-cmklookup" xreflabel="cmklookup">
+      <term><literal>cmklookup</literal></term>
+      <listitem>
+       <para>
+        This specifies how libpq should look up column master keys (CMKs) in
+        order to decrypt the column encryption keys (CEKs).
+        The value is a list of <literal>key=value</literal> entries separated
+        by semicolons.  Each key is the name of a key realm, or
+        <literal>*</literal> to match all realms.  The value is a
+        <literal>scheme:data</literal> specification.  The scheme specifies
+        the method to look up the key, the remaining data is specific to the
+        scheme.  Placeholders are replaced in the remaining data as follows:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>%a</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name (see <xref linkend="protocol-cmk-table"/>)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%r</literal></term>
+          <listitem>
+           <para>
+            The realm name
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        Available schemes are:
+        <variablelist>
+         <varlistentry>
+          <term><literal>file</literal></term>
+          <listitem>
+           <para>
+            Load the key material from a file.  The remaining data is the file
+            name.  Use this if the CMKs are kept in a file on the file system.
+           </para>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>run</literal></term>
+          <listitem>
+           <para>
+            Run the specified command to decrypt the CEK.  The remaining data
+            is a shell command.  Use this with key management systems that
+            perform the decryption themselves.  The command must print the
+            decrypted plaintext on the standard output.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        The default value is empty.
+       </para>
+
+       <para>
+        Example:
+<programlisting>
+cmklookup="r1=file:/some/where/secrets/%k.pem;*=file:/else/where/%r/%k.pem"
+</programlisting>
+        This specification says, for keys in realm <quote>r1</quote>, load
+        them from the specified file, replacing <literal>%k</literal> by the
+        key name.  For keys in other realms, load them from the file,
+        replacing realm and key names as specified.
+       </para>
+
+       <para>
+        An example for interacting with a (hypothetical) key management
+        system:
+<programlisting>
+cmklookup="*=run:acmekms decrypt --key %k --alg %a --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2999,32 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            half-byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this half-byte to one to force
+            encryption.  For example, use the C code literal
+            <literal>0x10</literal> to specify text format with forced
+            encryption.  If the array pointer is null then encryption is not
+            forced for any parameter.
+           </para>
+
+           <para>
+            Parameters corresponding to encrypted columns must be passed in
+            text format.  Specifying binary format for such a parameter will
+            result in an error.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter does not
+            correspond to an encrypted column on the server, then the call
+            will fail and the parameter will not be sent.  This can be used
+            for additional security against a compromised server.  (The
+            drawback is that application code then needs to be kept up to date
+            with knowledge about which columns are encrypted rather than
+            letting the server specify this.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3037,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3196,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Sends a request to execute a prepared statement with given
+        parameters, and waits for the result, with support for encrypted columns.
+<synopsis>
+PGresult *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> is like <xref
+        linkend="libpq-PQexecPrepared"/> with additional support for encrypted
+        columns.  The parameter <parameter>paramDesc</parameter> must be a
+        result set obtained from <xref linkend="libpq-PQdescribePrepared"/> on
+        the same prepared statement.
+       </para>
+
+       <para>
+        This function must be used if a statement parameter corresponds to an
+        underlying encrypted column.  In that situation, the prepared
+        statement needs to be described first so that libpq can obtain the
+        necessary key and other information from the server.  When that is
+        done, the parameters corresponding to encrypted columns are
+        automatically encrypted appropriately before being sent to the server.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4084,28 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQfisencrypted">
+     <term><function>PQfisencrypted</function><indexterm><primary>PQfisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given column came from an encrypted
+       column.  Column numbers start at 0.
+<synopsis>
+int PQfisencrypted(const PGresult *res,
+                   int column_number);
+</synopsis>
+      </para>
+
+      <para>
+       Encrypted column values are automatically decrypted, so this function
+       is not necessary to access the column value.  It can be used for extra
+       security to check whether the value was stored encrypted when one
+       thought it should be.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQfsize">
      <term><function>PQfsize</function><indexterm><primary>PQfsize</primary></indexterm></term>
 
@@ -4059,6 +4287,31 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQparamisencrypted">
+     <term><function>PQparamisencrypted</function><indexterm><primary>PQparamisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given parameter is destined for an
+       encrypted column.  Parameter numbers start at 0.
+<synopsis>
+int PQparamisencrypted(const PGresult *res, int param_number);
+</synopsis>
+      </para>
+
+      <para>
+       Values for parameters destined for encrypted columns are automatically
+       encrypted, so this function is not necessary to prepare the parameter
+       value.  It can be used for extra security to check whether the value
+       will be stored encrypted when one thought it should be.  (But see also
+       at <xref linkend="libpq-PQexecPreparedDescribed"/> for another way to do that.)
+       This function is only useful when inspecting the result of <xref
+       linkend="libpq-PQdescribePrepared"/>.  For other types of results it
+       will return false.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQprint">
      <term><function>PQprint</function><indexterm><primary>PQprint</primary></indexterm></term>
 
@@ -4584,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4845,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4902,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4963,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Sends a request to execute a prepared statement with given
+       parameters, without waiting for the result(s), with support for encrypted columns.
+<synopsis>
+int PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> is like <xref
+       linkend="libpq-PQsendQueryPrepared"/> with additional support for encrypted
+       columns.  The parameter <parameter>paramDesc</parameter> must be a
+       result set obtained from <xref linkend="libpq-PQsendDescribePrepared"/> on
+       the same prepared statement.
+      </para>
+
+      <para>
+       This function must be used if a statement parameter corresponds to an
+       underlying encrypted column.  In that situation, the prepared
+       statement needs to be described first so that libpq can obtain the
+       necessary key and other information from the server.  When that is
+       done, the parameters corresponding to encrypted columns are
+       automatically encrypted appropriately before being sent to the server.
+       See also under <xref linkend="libpq-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5052,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8086,26 @@ <title>Environment Variables</title>
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCMKLOOKUP</envar></primary>
+      </indexterm>
+      <envar>PGCMKLOOKUP</envar> behaves the same as the <xref
+      linkend="libpq-connect-cmklookup"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCOLUMNENCRYPTION</envar></primary>
+      </indexterm>
+      <envar>PGCOLUMNENCRYPTION</envar> behaves the same as the <xref
+      linkend="libpq-connect-column-encryption"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 73b7f4432f..c43c3051c3 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,76 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-column-encryption">
+   <title>Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    Automatic client-side column-level encryption is enabled by sending the
+    parameter <literal>_pq_.column_encryption</literal> with a value of
+    <literal>1</literal> in the StartupMessage.  This is a protocol extension
+    that enables a few additional protocol messages and adds additional fields
+    to existing protocol messages.  Client drivers should only activate this
+    protocol extension when requested by the user, for example through a
+    connection parameter.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the
+    messages ColumnMasterKey and ColumnEncryptionKey can appear before
+    RowDescription and ParameterDescription messages.  Clients should collect
+    the information in these messages and keep them for the duration of the
+    connection.  A server is not required to resend the key information for
+    each statement cycle if it was already sent during this connection.  If a
+    server resends a key that the client has already stored (that is, a key
+    having an ID equal to one already stored), the new information should
+    replace the old.  (This could happen, for example, if the key was altered
+    by server-side DDL commands.)
+   </para>
+
+   <para>
+    A client supporting automatic column-level encryption should automatically
+    decrypt the column value fields of DataRow messages corresponding to
+    encrypted columns, and it should automatically encrypt the parameter value
+    fields of Bind messages corresponding to encrypted columns.
+   </para>
+
+   <para>
+    When column encryption is used, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column-level encryption protocol option is enabled.  That way, a client
+    library only needs to support decrypting data sent in binary and does not
+    have to support decoding the text format of the encryption-related types
+    (see <xref linkend="datatype-encrypted"/>).
+   </para>
+
+   <para>
+    When deterministic encryption is used, clients need to take care to
+    represent plaintext to be encrypted in a consistent form.  For example,
+    encrypting an integer represented by the string <literal>100</literal> and
+    an integer represented by the string <literal>+100</literal> would result
+    in two different ciphertexts, thus defeating the main point of
+    deterministic encryption.  This protocol specification requires the
+    plaintext to be in <quote>canonical</quote> form, which is the form that
+    is produced by the server when it outputs a particular value in text
+    format.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the client
+    encoding must match the server encoding.  This ensures that all values
+    encrypted or decrypted by the client match the server encoding.
+   </para>
+
+   <para>
+    The cryptographic operations used for automatic client-side column-level
+    encryption are described in <xref
+    linkend="protocol-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -3841,6 +3911,16 @@ <title>Message Formats</title>
          The parameter format codes.  Each must presently be
          zero (text) or one (binary).
         </para>
+
+        <para>
+         If the protocol extension <literal>_pq_.column_encryption</literal>
+         is enabled (see <xref linkend="protocol-flow-column-encryption"/>),
+         then the second-least-significant half-byte is set to one if the
+         parameter was encrypted by the client.  (So, for example, to send an
+         encrypted value in binary, the field is set to 0x11 in total.)  This
+         is used by the server to check that a parameter that was required to
+         be encrypted was actually encrypted.
+        </para>
        </listitem>
       </varlistentry>
 
@@ -4061,6 +4141,140 @@ <title>Message Formats</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ColumnEncryptionKey">
+    <term>ColumnEncryptionKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('Y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column encryption key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the master key used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the algorithm used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The length of the following key material.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Byte<replaceable>n</replaceable></term>
+       <listitem>
+        <para>
+         The key material, encrypted with the master key referenced above.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="protocol-message-formats-ColumnMasterKey">
+    <term>ColumnMasterKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column master key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The key's realm.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="protocol-message-formats-CommandComplete">
     <term>CommandComplete (B)</term>
     <listitem>
@@ -5164,6 +5378,45 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each parameter:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         This is used as a bit field of flags.  If the parameter is to be
+         encrypted and bit 0x0001 is set, the column underlying the parameter
+         uses deterministic encryption, otherwise randomized encryption.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5552,6 +5805,50 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each field:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         This is used as a bit field of flags.  If the field is encrypted and
+         bit 0x0001 is set, the field uses deterministic encryption, otherwise
+         randomized encryption.
+        </para>
+        <!--
+            This is not really useful here, but it keeps alignment with
+            ParameterDescription.  Future flags might be useful in both
+            places.
+        -->
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7377,6 +7674,176 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-column-encryption-crypto">
+  <title>Automatic Client-side Column-level Encryption Cryptography</title>
+
+  <para>
+   This section describes the cryptographic operations used by the automatic
+   client-side column-level encryption functionality.  A client that supports
+   this functionality needs to implement these operations as specified here in
+   order to be able to interoperate with other clients.
+  </para>
+
+  <para>
+   Column encryption key algorithms and column master key algorithms are
+   identified by integers in the protocol messages and the system catalogs.
+   Additional algorithms may be added to this protocol specification without a
+   change in the protocol version number.  Clients should implement support
+   for all the algorithms specified here.  If a client encounters an algorithm
+   identifier it does not recognize or does not support, it must raise an
+   error.  A suitable error message should be provided to the application or
+   user.
+  </para>
+
+  <sect2 id="protocol-cmk">
+   <title>Column Master Keys</title>
+
+   <para>
+    The currently defined algorithms for column master keys are listed in
+    <xref linkend="protocol-cmk-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC
+       8017</ulink>/PKCS #1)</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect2>
+
+  <sect2 id="protocol-cek">
+   <title>Column Encryption Keys</title>
+
+   <para>
+    The currently defined algorithms for column encryption keys are listed in
+    <xref linkend="protocol-cek-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</entry>
+       <entry><literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</entry>
+       <entry><literal>AEAD_AES_192_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_512</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <para>
+    The <quote>associated data</quote> in these algorithms consists of 4
+    bytes: The ASCII letters <literal>P</literal> and <literal>G</literal>
+    (byte values 80 and 71), followed by the version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </para>
+
+   <para>
+    The length of the initialization vector is 16 octets for all CEK algorithm
+    variants.  For randomized encryption, the initialization vector should be
+    (cryptographically strong) random bytes.  For deterministic encryption,
+    the initialization vector is constructed as
+<programlisting>
+SUBSTRING(<replaceable>HMAC</replaceable>(<replaceable>K</replaceable>, <replaceable>P</replaceable>) FOR <replaceable>IVLEN</replaceable>)
+</programlisting>
+    where <replaceable>HMAC</replaceable> is the HMAC function associated with
+    the algorithm, <replaceable>K</replaceable> is the IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </para>
+  </sect2>
+ </sect1>
+
  <sect1 id="protocol-changes">
   <title>Summary of Changes since Protocol 2.0</title>
 
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 54b5f22d6e..a730e5d650 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -8,6 +8,8 @@
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
+<!ENTITY alterColumnEncryptionKey SYSTEM "alter_column_encryption_key.sgml">
+<!ENTITY alterColumnMasterKey SYSTEM "alter_column_master_key.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
 <!ENTITY alterDefaultPrivileges SYSTEM "alter_default_privileges.sgml">
@@ -62,6 +64,8 @@
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
+<!ENTITY createColumnEncryptionKey SYSTEM "create_column_encryption_key.sgml">
+<!ENTITY createColumnMasterKey SYSTEM "create_column_master_key.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
 <!ENTITY createDatabase     SYSTEM "create_database.sgml">
 <!ENTITY createDomain       SYSTEM "create_domain.sgml">
@@ -109,6 +113,8 @@
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
+<!ENTITY dropColumnEncryptionKey SYSTEM "drop_column_encryption_key.sgml">
+<!ENTITY dropColumnMasterKey SYSTEM "drop_column_master_key.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
 <!ENTITY dropDatabase       SYSTEM "drop_database.sgml">
 <!ENTITY dropDomain         SYSTEM "drop_domain.sgml">
diff --git a/doc/src/sgml/ref/alter_column_encryption_key.sgml b/doc/src/sgml/ref/alter_column_encryption_key.sgml
new file mode 100644
index 0000000000..655e1e00d8
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+doc/src/sgml/ref/alter_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-encryption-key">
+ <indexterm zone="sql-alter-column-encryption-key">
+  <primary>ALTER COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>change the definition of a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> ADD VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> DROP VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN ENCRYPTION KEY</command> changes the definition of a
+   column encryption key.
+  </para>
+
+  <para>
+   The first form adds new encrypted key data to a column encryption key,
+   which must be encrypted with a different column master key than the
+   existing key data.  The second form removes a key data entry for a given
+   column master key.  Together, these forms can be used for column master key
+   rotation.
+  </para>
+
+  <para>
+   You must own the column encryption key to use <command>ALTER COLUMN
+   ENCRYPTION KEY</command>.  To alter the owner, you must also be a direct or
+   indirect member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column encryption key's
+   schema.  (These restrictions enforce that altering the owner doesn't do
+   anything you couldn't do by dropping and recreating the column encryption
+   key.  However, a superuser can alter ownership of any column encryption key
+   anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column encryption
+      key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  See <xref
+      linkend="sql-create-column-encryption-key"/> for details
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rotate the master keys used to encrypt a given column encryption key,
+   use a command sequence like this:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (
+    COLUMN_MASTER_KEY = cmk1
+);
+</programlisting>
+  </para>
+
+  <para>
+   To rename the column encryption key <literal>cek1</literal> to
+   <literal>cek2</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column encryption key <literal>cek1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN ENCRYPTION KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/alter_column_master_key.sgml b/doc/src/sgml/ref/alter_column_master_key.sgml
new file mode 100644
index 0000000000..7f0e656ef0
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+doc/src/sgml/ref/alter_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-master-key">
+ <indexterm zone="sql-alter-column-master-key">
+  <primary>ALTER COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN MASTER KEY</refname>
+  <refpurpose>change the definition of a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> ( REALM = <replaceable>realm</replaceable> )
+
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </para>
+
+  <para>
+   You must own the column master key to use <command>ALTER COLUMN MASTER
+   KEY</command>.  To alter the owner, you must also be a direct or indirect
+   member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column master key's schema.
+   (These restrictions enforce that altering the owner doesn't do anything you
+   couldn't do by dropping and recreating the column master key.  However, a
+   superuser can alter ownership of any column master key anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the column master key <literal>cmk1</literal> to
+   <literal>cmk2</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column master key <literal>cmk1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN MASTER KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..1caf9bfa56 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..ebcbf5d00a 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,16 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support automatic client-side
+    column-level encryption or decryption; its input or output data will
+    always be the ciphertext.  This is usually suitable for backups (see also
+    <xref linkend="app-pgdump"/>).  If automatic client-side encryption or
+    decryption is wanted, <command>INSERT</command> and
+    <command>SELECT</command> need to be used instead to write and read the
+    data.
+   </para>
+
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/ref/create_column_encryption_key.sgml b/doc/src/sgml/ref/create_column_encryption_key.sgml
new file mode 100644
index 0000000000..65534fb03f
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,173 @@
+<!--
+doc/src/sgml/ref/create_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-encryption-key">
+ <indexterm zone="sql-create-column-encryption-key">
+  <primary>CREATE COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>define a new column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN ENCRYPTION KEY <replaceable>name</replaceable> WITH VALUES (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    ALGORITHM = <replaceable>algorithm</replaceable>,
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+[ , ... ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN ENCRYPTION KEY</command> defines a new column
+   encryption key.  A column encryption key is used for client-side encryption
+   of table columns that have been defined as encrypted.  The key material of
+   a column encryption key is stored in the database's system catalogs,
+   encrypted (wrapped) by a column master key (which in turn is only
+   accessible to the client, not the database server).
+  </para>
+
+  <para>
+   A column encryption key can be associated with more than one column master
+   key.  To specify that, specify more than one parenthesized definition (see
+   also the examples).
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column encryption key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.  You must have <literal>USAGE</literal> privilege on the
+      column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  Supported algorithms are:
+      <itemizedlist>
+       <listitem>
+        <para><literal>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></para>
+       </listitem>
+      </itemizedlist>
+     </para>
+
+     <para>
+      This is informational only.  The specified value is provided to the
+      client, which may use it for decrypting the column encryption key on the
+      client side.  But a client is also free to ignore this information and
+      figure out how to arrange the decryption in some other way.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+</programlisting>
+  </para>
+
+  <para>
+   To specify more than one associated column master key:
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\xF1F2F2F4...'
+);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_column_master_key.sgml b/doc/src/sgml/ref/create_column_master_key.sgml
new file mode 100644
index 0000000000..6aaa1088d1
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+doc/src/sgml/ref/create_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-master-key">
+ <indexterm zone="sql-create-column-master-key">
+  <primary>CREATE COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN MASTER KEY</refname>
+  <refpurpose>define a new column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN MASTER KEY <replaceable>name</replaceable> [ WITH (
+    [ REALM = <replaceable>realm</replaceable> ]
+) ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN MASTER KEY</command> defines a new column master
+   key.  A column master key is used to encrypt column encryption keys, which
+   are the keys that actually encrypt the column data.  The key material of
+   the column master key is not stored in the database.  The definition of a
+   column master key records information that will allow a client to locate
+   the key material, for example in a file or in a key management system.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column master key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>realm</replaceable></term>
+
+    <listitem>
+     <para>
+      This is an optional string that can be used to organize column master
+      keys into groups for lookup by clients.  The intent is that all column
+      master keys that are stored in the same system (file system location,
+      key management system, etc.) should be in the same realm.  A client
+      would then be configured to look up all keys in a given realm in a
+      certain way.  See the documentation of the respective client library for
+      further usage instructions.
+     </para>
+
+     <para>
+      The default is the empty string.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+<programlisting>
+CREATE COLUMN MASTER KEY cmk1 (realm = 'myrealm');
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index a03dee4afe..d1549c7f45 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -22,7 +22,7 @@
  <refsynopsisdiv>
 <synopsis>
 CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable> ( [
-  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,47 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables automatic client-side column-level encryption for the column.
+      <replaceable>encryption_options</replaceable> are comma-separated
+      <literal>key=value</literal> specifications.  The following options are
+      available:
+      <variablelist>
+       <varlistentry>
+        <term><literal>column_encryption_key</literal></term>
+        <listitem>
+         <para>
+          Specifies the name of the column encryption key to use.  Specifying
+          this is mandatory.  You must have <literal>USAGE</literal> privilege
+          on the column encryption key.
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>encryption_type</literal></term>
+        <listitem>
+         <para>
+          <literal>randomized</literal> (the default) or <literal>deterministic</literal>
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>algorithm</literal></term>
+        <listitem>
+         <para>
+          The encryption algorithm to use.  The default is
+          <literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -704,6 +745,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</literal></term>
         <listitem>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523ca..6a94706ef7 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@
 
  <refsynopsisdiv>
 <synopsis>
-DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
+DISCARD { ALL | COLUMN ENCRYPTION KEYS | PLANS | SEQUENCES | TEMPORARY | TEMP }
 </synopsis>
  </refsynopsisdiv>
 
@@ -42,6 +42,17 @@ <title>Parameters</title>
 
   <variablelist>
 
+   <varlistentry>
+    <term><literal>COLUMN ENCRYPTION KEYS</literal></term>
+    <listitem>
+     <para>
+      Discards knowledge about which column encryption keys and column master
+      keys have been sent to the client in this session.  (They will
+      subsequently be re-sent as required.)
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>PLANS</literal></term>
     <listitem>
@@ -93,6 +104,7 @@ <title>Parameters</title>
 DISCARD PLANS;
 DISCARD TEMP;
 DISCARD SEQUENCES;
+DISCARD COLUMN ENCRYPTION KEYS;
 </programlisting></para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/ref/drop_column_encryption_key.sgml b/doc/src/sgml/ref/drop_column_encryption_key.sgml
new file mode 100644
index 0000000000..f2ac1beb08
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_encryption_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-encryption-key">
+ <indexterm zone="sql-drop-column-encryption-key">
+  <primary>DROP COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>remove a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN ENCRYPTION KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN ENCRYPTION KEY</command> removes a previously defined
+   column encryption key.  To be able to drop a column encryption key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column encryption key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column encryption key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column encryption key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column encryption key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN ENCRYPTION KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_column_master_key.sgml b/doc/src/sgml/ref/drop_column_master_key.sgml
new file mode 100644
index 0000000000..fae95e09d1
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_master_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-master-key">
+ <indexterm zone="sql-drop-column-master-key">
+  <primary>DROP COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN MASTER KEY</refname>
+  <refpurpose>remove a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN MASTER KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN MASTER KEY</command> removes a previously defined
+   column master key.  To be able to drop a column master key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column master key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column master key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column master key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column master key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN MASTER KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 35bf0332c8..f712f8e9e4 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -46,6 +46,16 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN ENCRYPTION KEY <replaceable>cek_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN MASTER KEY <replaceable>cmk_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON DOMAIN <replaceable>domain_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -513,7 +523,7 @@ <title>Compatibility</title>
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
+    Privileges on databases, tablespaces, schemas, keys, languages, and
     configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 49d218905f..536f10def0 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -716,6 +716,48 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option turns on the column encryption connection option in
+        <application>libpq</application> (see <xref
+        linkend="libpq-connect-column-encryption"/>).  Column master key
+        lookup must be configured by the user, either through a connection
+        option or an environment setting (see <xref
+        linkend="libpq-connect-cmklookup"/>).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  (But then it is recommended
+        to not do this on the same host as the server, to avoid exposing
+        unencrypted data that is meant to be kept encrypted on the server.)
+        Note that a dump created with this option cannot be restored into a
+        database with column encryption.
+       </para>
+       <!--
+           XXX: The latter would require another pg_dump option to put out
+           INSERT ... \bind ... \g commands.
+       -->
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index e62d05e5ab..4bf60c729f 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -259,6 +259,33 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  Note that a dump created
+        with this option cannot be restored into a database with column
+        encryption.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index dc6528dc11..e68b8440be 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4026,6 +4054,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <term><varname>HIDE_COLUMN_ENCRYPTION</varname></term>
+        <listitem>
+        <para>
+         If this variable is set to <literal>true</literal>, column encryption
+         details are not displayed. This is mainly useful for regression
+         tests.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..c898997915 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -36,6 +36,8 @@ <title>SQL Commands</title>
    &abort;
    &alterAggregate;
    &alterCollation;
+   &alterColumnEncryptionKey;
+   &alterColumnMasterKey;
    &alterConversion;
    &alterDatabase;
    &alterDefaultPrivileges;
@@ -90,6 +92,8 @@ <title>SQL Commands</title>
    &createAggregate;
    &createCast;
    &createCollation;
+   &createColumnEncryptionKey;
+   &createColumnMasterKey;
    &createConversion;
    &createDatabase;
    &createDomain;
@@ -137,6 +141,8 @@ <title>SQL Commands</title>
    &dropAggregate;
    &dropCast;
    &dropCollation;
+   &dropColumnEncryptionKey;
+   &dropColumnMasterKey;
    &dropConversion;
    &dropDatabase;
    &dropDomain;
diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index ef818228ac..c5894893eb 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -20,7 +20,9 @@
 
 #include "access/printsimple.h"
 #include "catalog/pg_type.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 
 /*
@@ -46,6 +48,12 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc)
 		pq_sendint16(&buf, attr->attlen);
 		pq_sendint32(&buf, attr->atttypmod);
 		pq_sendint16(&buf, 0);	/* format code */
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&buf, 0);	/* CEK */
+			pq_sendint32(&buf, 0);	/* CEK alg */
+			pq_sendint16(&buf, 0);	/* flags */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 72faeb5dfa..2d627b6f47 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,28 @@
  */
 #include "postgres.h"
 
+#include "access/genam.h"
 #include "access/printtup.h"
+#include "access/skey.h"
+#include "access/table.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
 #include "libpq/libpq.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memdebug.h"
 #include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
 
 
 static void printtup_startup(DestReceiver *self, int operation,
@@ -151,6 +166,166 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 	 */
 }
 
+/*
+ * Send ColumnMasterKey message, unless it's already been sent in this session
+ * for this key.
+ */
+List	   *cmk_sent = NIL;
+
+static void
+cmk_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+}
+
+static void
+MaybeSendColumnMasterKeyMessage(Oid cmkid)
+{
+	HeapTuple	tuple;
+	Form_pg_colmasterkey cmkform;
+	Datum		datum;
+	bool		isnull;
+	StringInfoData buf;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cmk_sent, cmkid))
+		return;
+
+	tuple = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	cmkform = (Form_pg_colmasterkey) GETSTRUCT(tuple);
+
+	pq_beginmessage(&buf, 'y'); /* ColumnMasterKey */
+	pq_sendint32(&buf, cmkform->oid);
+	pq_sendstring(&buf, NameStr(cmkform->cmkname));
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkrealm, &isnull);
+	Assert(!isnull);
+	pq_sendstring(&buf, TextDatumGetCString(datum));
+	pq_endmessage(&buf);
+
+	ReleaseSysCache(tuple);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CMKOID, cmk_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cmk_sent = lappend_oid(cmk_sent, cmkid);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * Send ColumnEncryptionKey message, unless it's already been sent in this
+ * session for this key.
+ */
+List	   *cek_sent = NIL;
+
+static void
+cek_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
+void
+MaybeSendColumnEncryptionKeyMessage(Oid attcek)
+{
+	HeapTuple	tuple;
+	ScanKeyData skey[1];
+	SysScanDesc sd;
+	Relation	rel;
+	bool		found = false;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cek_sent, attcek))
+		return;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	ScanKeyInit(&skey[0],
+				Anum_pg_colenckeydata_ckdcekid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(attcek));
+	rel = table_open(ColumnEncKeyDataRelationId, AccessShareLock);
+	sd = systable_beginscan(rel, ColumnEncKeyCekidCmkidIndexId, true, NULL, 1, skey);
+
+	while ((tuple = systable_getnext(sd)))
+	{
+		Form_pg_colenckeydata ckdform = (Form_pg_colenckeydata) GETSTRUCT(tuple);
+		Datum		datum;
+		bool		isnull;
+		bytea	   *ba;
+		StringInfoData buf;
+
+		MaybeSendColumnMasterKeyMessage(ckdform->ckdcmkid);
+
+		datum = heap_getattr(tuple, Anum_pg_colenckeydata_ckdencval, RelationGetDescr(rel), &isnull);
+		Assert(!isnull);
+		ba = pg_detoast_datum_packed((bytea *) DatumGetPointer(datum));
+
+		pq_beginmessage(&buf, 'Y'); /* ColumnEncryptionKey */
+		pq_sendint32(&buf, ckdform->ckdcekid);
+		pq_sendint32(&buf, ckdform->ckdcmkid);
+		pq_sendint32(&buf, ckdform->ckdcmkalg);
+		pq_sendint32(&buf, VARSIZE_ANY_EXHDR(ba));
+		pq_sendbytes(&buf, VARDATA_ANY(ba), VARSIZE_ANY_EXHDR(ba));
+		pq_endmessage(&buf);
+
+		found = true;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	systable_endscan(sd);
+	table_close(rel, NoLock);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CEKDATAOID, cek_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cek_sent = lappend_oid(cek_sent, attcek);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+void
+DiscardColumnEncryptionKeys(void)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -167,6 +342,7 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 						  List *targetlist, int16 *formats)
 {
 	int			natts = typeinfo->natts;
+	size_t		sz;
 	int			i;
 	ListCell   *tlist_item = list_head(targetlist);
 
@@ -183,14 +359,18 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 	 * Have to overestimate the size of the column-names, to account for
 	 * character set overhead.
 	 */
-	enlargeStringInfo(buf, (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
-							+ sizeof(Oid)	/* resorigtbl */
-							+ sizeof(AttrNumber)	/* resorigcol */
-							+ sizeof(Oid)	/* atttypid */
-							+ sizeof(int16) /* attlen */
-							+ sizeof(int32) /* attypmod */
-							+ sizeof(int16) /* format */
-							) * natts);
+	sz = (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
+		  + sizeof(Oid)	/* resorigtbl */
+		  + sizeof(AttrNumber)	/* resorigcol */
+		  + sizeof(Oid)	/* atttypid */
+		  + sizeof(int16) /* attlen */
+		  + sizeof(int32) /* attypmod */
+		  + sizeof(int16)); /* format */
+	if (MyProcPort->column_encryption_enabled)
+		sz += (sizeof(int32)		/* attcekid */
+			   + sizeof(int32)		/* attencalg */
+			   + sizeof(int16));	/* flags */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +380,9 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
+		int16		flags = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +414,31 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(atttypid))
+		{
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(resorigtbl), Int16GetDatum(resorigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", resorigcol, resorigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			atttypid = orig_att->attusertypid;
+			atttypmod = orig_att->attusertypmod;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->atttypmod;
+			if (orig_att->atttypid == PG_ENCRYPTED_DETOID)
+				flags |= 0x0001;
+			ReleaseSysCache(tp);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +446,12 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		pq_writeint16(buf, att->attlen);
 		pq_writeint32(buf, atttypmod);
 		pq_writeint16(buf, format);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_writeint32(buf, attcekid);
+			pq_writeint32(buf, attencalg);
+			pq_writeint16(buf, flags);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +485,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 72a2c3d3db..f86ba299c3 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attusertypid != attr2->attusertypid)
+			return false;
+		if (attr1->attusertypmod != attr2->attusertypmod)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 24bab58499..cc48069932 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index a60107bf94..7b9575635b 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index c4232344aa..a3547c6cae 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_default_acl.h"
@@ -247,6 +249,12 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_SEQUENCE:
 			whole_mask = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			whole_mask = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			whole_mask = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			whole_mask = ACL_ALL_RIGHTS_DATABASE;
 			break;
@@ -473,6 +481,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
 			errormsg = gettext_noop("invalid privilege type %s for sequence");
 			break;
+		case OBJECT_CEK:
+			all_privileges = ACL_ALL_RIGHTS_CEK;
+			errormsg = gettext_noop("invalid privilege type %s for column encryption key");
+			break;
+		case OBJECT_CMK:
+			all_privileges = ACL_ALL_RIGHTS_CMK;
+			errormsg = gettext_noop("invalid privilege type %s for column master key");
+			break;
 		case OBJECT_DATABASE:
 			all_privileges = ACL_ALL_RIGHTS_DATABASE;
 			errormsg = gettext_noop("invalid privilege type %s for database");
@@ -597,6 +613,12 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_SEQUENCE:
 			ExecGrant_Relation(istmt);
 			break;
+		case OBJECT_CEK:
+			ExecGrant_common(istmt, ColumnEncKeyRelationId, ACL_ALL_RIGHTS_CEK, NULL);
+			break;
+		case OBJECT_CMK:
+			ExecGrant_common(istmt, ColumnMasterKeyRelationId, ACL_ALL_RIGHTS_CMK, NULL);
+			break;
 		case OBJECT_DATABASE:
 			ExecGrant_common(istmt, DatabaseRelationId, ACL_ALL_RIGHTS_DATABASE, NULL);
 			break;
@@ -676,6 +698,26 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 				objects = lappend_oid(objects, relOid);
 			}
 			break;
+		case OBJECT_CEK:
+			foreach(cell, objnames)
+			{
+				List	   *cekname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cek_oid(cekname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
+		case OBJECT_CMK:
+			foreach(cell, objnames)
+			{
+				List	   *cmkname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cmk_oid(cmkname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
 		case OBJECT_DATABASE:
 			foreach(cell, objnames)
 			{
@@ -2693,6 +2735,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("permission denied for column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("permission denied for column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -2798,6 +2846,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -2828,6 +2877,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("must be owner of aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("must be owner of column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("must be owner of column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("must be owner of collation %s");
 						break;
@@ -2938,6 +2993,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3019,6 +3075,10 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
+		case OBJECT_CEK:
+			return object_aclmask(ColumnEncKeyRelationId, object_oid, roleid, mask, how);
+		case OBJECT_CMK:
+			return object_aclmask(ColumnMasterKeyRelationId, object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
 		case OBJECT_FUNCTION:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..cab6cfd140 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -30,7 +30,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -153,6 +156,9 @@ static const Oid object_classes[] = {
 	TypeRelationId,				/* OCLASS_TYPE */
 	CastRelationId,				/* OCLASS_CAST */
 	CollationRelationId,		/* OCLASS_COLLATION */
+	ColumnEncKeyRelationId,		/* OCLASS_CEK */
+	ColumnEncKeyDataRelationId,	/* OCLASS_CEKDATA */
+	ColumnMasterKeyRelationId,	/* OCLASS_CMK */
 	ConstraintRelationId,		/* OCLASS_CONSTRAINT */
 	ConversionRelationId,		/* OCLASS_CONVERSION */
 	AttrDefaultRelationId,		/* OCLASS_DEFAULT */
@@ -1493,6 +1499,9 @@ doDeletion(const ObjectAddress *object, int flags)
 
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONVERSION:
 		case OCLASS_LANGUAGE:
 		case OCLASS_OPCLASS:
@@ -2859,6 +2868,15 @@ getObjectClass(const ObjectAddress *object)
 		case CollationRelationId:
 			return OCLASS_COLLATION;
 
+		case ColumnEncKeyRelationId:
+			return OCLASS_CEK;
+
+		case ColumnEncKeyDataRelationId:
+			return OCLASS_CEKDATA;
+
+		case ColumnMasterKeyRelationId:
+			return OCLASS_CMK;
+
 		case ConstraintRelationId:
 			return OCLASS_CONSTRAINT;
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 4f006820b8..df282c796f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypid - 1] = ObjectIdGetDatum(attrs->attusertypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypmod - 1] = Int32GetDatum(attrs->attusertypmod);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attusertypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attusertypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 14e57adee2..00f914bc5f 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -26,6 +26,8 @@
 #include "catalog/dependency.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1997,6 +1999,254 @@ OpfamilyIsVisible(Oid opfid)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4567,6 +4817,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(OpfamilyIsVisible(oid));
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2f688166e1..6c4ab9ac59 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -191,6 +194,48 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_COLLATION,
 		true
 	},
+	{
+		"column encryption key",
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		CEKOID,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		Anum_pg_colenckey_cekowner,
+		Anum_pg_colenckey_cekacl,
+		OBJECT_CEK,
+		true
+	},
+	{
+		"column encryption key data",
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		CEKDATAOID,
+		-1,
+		Anum_pg_colenckeydata_oid,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		-1,
+		false
+	},
+	{
+		"column master key",
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		CMKOID,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		Anum_pg_colmasterkey_cmkowner,
+		Anum_pg_colmasterkey_cmkacl,
+		OBJECT_CMK,
+		true
+	},
 	{
 		"constraint",
 		ConstraintRelationId,
@@ -723,6 +768,18 @@ static const struct object_type_map
 	{
 		"collation", OBJECT_COLLATION
 	},
+	/* OCLASS_CEK */
+	{
+		"column encryption key", OBJECT_CEK
+	},
+	/* OCLASS_CEKDATA */
+	{
+		"column encryption key data", OBJECT_CEKDATA
+	},
+	/* OCLASS_CMK */
+	{
+		"column master key", OBJECT_CMK
+	},
 	/* OCLASS_CONSTRAINT */
 	{
 		"table constraint", OBJECT_TABCONSTRAINT
@@ -1029,6 +1086,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1108,6 +1175,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, castNode(List, object));
+					Oid			cekid;
+					Oid			cmkid;
+
+					cekid = get_cek_oid(cekname, missing_ok);
+					cmkid = get_cmk_oid(cmkname, missing_ok);
+
+					address.classId = ColumnEncKeyDataRelationId;
+					address.objectId = get_cekdata_oid(cekid, cmkid, missing_ok);
+					address.objectSubId = 0;
+				}
+				break;
 			case OBJECT_TRANSFORM:
 				{
 					TypeName   *typename = linitial_node(TypeName, castNode(List, object));
@@ -2311,6 +2393,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2367,6 +2451,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2489,6 +2574,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2575,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3040,6 +3128,92 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CEKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata cekdata;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+
+				cekdata = (Form_pg_colenckeydata) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("column encryption key data of %s for %s"),
+								 getObjectDescription(&(ObjectAddress){ColumnEncKeyRelationId, cekdata->ckdcekid}, false),
+								 getObjectDescription(&(ObjectAddress){ColumnMasterKeyRelationId, cekdata->ckdcmkid}, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4440,6 +4614,18 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "collation");
 			break;
 
+		case OCLASS_CEK:
+			appendStringInfoString(&buffer, "column encryption key");
+			break;
+
+		case OCLASS_CEKDATA:
+			appendStringInfoString(&buffer, "column encryption key data");
+			break;
+
+		case OCLASS_CMK:
+			appendStringInfoString(&buffer, "column master key");
+			break;
+
 		case OCLASS_CONSTRAINT:
 			getConstraintTypeDescription(&buffer, object->objectId,
 										 missing_ok);
@@ -4905,6 +5091,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata form;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckeydata) GETSTRUCT(tup);
+				appendStringInfo(&buffer,
+								 "of %s for %s",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column master key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91..69f6175c60 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -19,6 +19,7 @@ OBJS = \
 	analyze.o \
 	async.o \
 	cluster.o \
+	colenccmds.o \
 	collationcmds.o \
 	comment.o \
 	constraint.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index bea51b3af1..59c23e9ef8 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -22,7 +22,9 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_foreign_data_wrapper.h"
@@ -118,6 +120,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -379,6 +387,8 @@ ExecRenameStmt(RenameStmt *stmt)
 			return RenameType(stmt);
 
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_EVENT_TRIGGER:
@@ -527,6 +537,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -643,6 +655,9 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 			break;
 
 		case OCLASS_CAST:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
@@ -876,6 +891,8 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/colenccmds.c b/src/backend/commands/colenccmds.c
new file mode 100644
index 0000000000..3ada6d5aeb
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,439 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/colenccmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+static void
+parse_cek_attributes(ParseState *pstate, List *definition, Oid *cmkoid_p, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	Assert(cmkoid_p);
+
+	foreach(lc, definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "column_master_key") == 0)
+			defelp = &cmkEl;
+		else if (strcmp(defel->defname, "algorithm") == 0)
+			defelp = &algEl;
+		else if (strcmp(defel->defname, "encrypted_value") == 0)
+			defelp = &encvalEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column encryption key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (cmkEl)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("attribute \"%s\" must be specified",
+						"column_master_key")));
+
+	if (algEl)
+	{
+		char	   *val = defGetString(algEl);
+
+		if (!alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"algorithm")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	if (encvalEl)
+	{
+		if (!encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"encrypted_value")));
+
+		encval = defGetString(encvalEl);
+	}
+	else
+	{
+		if (encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"encrypted_value")));
+	}
+
+	*cmkoid_p = cmkoid;
+	if (alg_p)
+		*alg_p = alg;
+	if (encval_p)
+		*encval_p = encval;
+}
+
+static void
+insert_cekdata_record(Oid cekoid, Oid cmkoid, int alg, char *encval)
+{
+	Oid			cekdataoid;
+	Relation	rel;
+	Datum		values[Natts_pg_colenckeydata] = {0};
+	bool		nulls[Natts_pg_colenckeydata] = {0};
+	HeapTuple	tup;
+	ObjectAddress myself;
+	ObjectAddress other;
+
+	rel = table_open(ColumnEncKeyDataRelationId, RowExclusiveLock);
+
+	cekdataoid = GetNewOidWithIndex(rel, ColumnEncKeyDataOidIndexId, Anum_pg_colenckeydata_oid);
+	values[Anum_pg_colenckeydata_oid - 1] = ObjectIdGetDatum(cekdataoid);
+	values[Anum_pg_colenckeydata_ckdcekid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckeydata_ckdcmkid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colenckeydata_ckdcmkalg - 1] = Int32GetDatum(alg);
+	values[Anum_pg_colenckeydata_ckdencval - 1] = DirectFunctionCall1(byteain, CStringGetDatum(encval));
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyDataRelationId, cekdataoid);
+
+	/* dependency cekdata -> cek */
+	ObjectAddressSet(other, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_AUTO);
+
+	/* dependency cekdata -> cmk */
+	ObjectAddressSet(other, ColumnMasterKeyRelationId, cmkoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_NORMAL);
+
+	table_close(rel, NoLock);
+}
+
+ObjectAddress
+CreateCEK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+	nulls[Anum_pg_colenckey_cekacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOnOwner(ColumnEncKeyRelationId, cekoid, GetUserId());
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnEncKeyRelationId, cekoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt)
+{
+	Oid			cekoid;
+	ObjectAddress address;
+
+	cekoid = get_cek_oid(stmt->cekname, false);
+
+	if (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(stmt->cekname));
+
+	if (stmt->isDrop)
+	{
+		Oid			cmkoid = 0;
+		Oid			cekdataoid;
+		ObjectAddress obj;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, NULL, NULL);
+		cekdataoid = get_cekdata_oid(cekoid, cmkoid, false);
+		ObjectAddressSet(obj, ColumnEncKeyDataRelationId, cekdataoid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+		AclResult	aclresult;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		if (get_cekdata_oid(cekoid, cmkoid, true))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("column encryption key \"%s\" already has data for master key \"%s\"",
+						   NameListToString(stmt->cekname), get_cmk_name(cmkoid, false)));
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	InvokeObjectPostAlterHook(ColumnEncKeyRelationId, cekoid, 0);
+	ObjectAddressSet(address, ColumnEncKeyRelationId, cekoid);
+
+	return address;
+}
+
+ObjectAddress
+CreateCMK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+		realm = defGetString(realmEl);
+	else
+		realm = "";
+
+	cmkoid = GetNewOidWithIndex(rel, ColumnMasterKeyOidIndexId, Anum_pg_colmasterkey_oid);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+	nulls[Anum_pg_colmasterkey_cmkacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	recordDependencyOnOwner(ColumnMasterKeyRelationId, cmkoid, GetUserId());
+
+	ObjectAddressSet(myself, ColumnMasterKeyRelationId, cmkoid);
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d6c6d514f3..9685b7886a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 296dc82d2e..86d22ca065 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/printtup.h"
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "commands/async.h"
@@ -25,7 +26,7 @@
 static void DiscardAll(bool isTopLevel);
 
 /*
- * DISCARD { ALL | SEQUENCES | TEMP | PLANS }
+ * DISCARD
  */
 void
 DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
@@ -36,6 +37,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			DiscardAll(isTopLevel);
 			break;
 
+		case DISCARD_COLUMN_ENCRYPTION_KEYS:
+			DiscardColumnEncryptionKeys();
+			break;
+
 		case DISCARD_PLANS:
 			ResetPlanCache();
 			break;
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	DiscardColumnEncryptionKeys();
 }
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 82bda15889..b4c681005a 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +517,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..4c1628cf7b 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,9 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -1027,6 +1030,9 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_TYPE:
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_CONVERSION:
 		case OCLASS_DEFAULT:
@@ -2056,6 +2062,9 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2139,6 +2148,9 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 42cced9ebe..4b5ac30441 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..93d61c96ad 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,9 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 62d9917ca3..c30b59265b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,16 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			AclResult	aclresult;
+
+			GetColumnEncryption(colDef->encryption, attr);
+			aclresult = object_aclcheck(ColumnEncKeyRelationId, attr->attcek, GetUserId(), ACL_USAGE);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attr->attcek, false));
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -2562,13 +2575,43 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				Oid			defCollId;
 
 				/*
-				 * Yes, try to merge the two column definitions. They must
-				 * have the same type, typmod, and collation.
+				 * Yes, try to merge the two column definitions.
 				 */
 				ereport(NOTICE,
 						(errmsg("merging multiple inherited definitions of column \"%s\"",
 								attributeName)));
 				def = (ColumnDef *) list_nth(inhSchema, exist_attno - 1);
+
+				/*
+				 * Check encryption parameter.  All parents must have the same
+				 * encryption settings for a column.
+				 */
+				if ((def->encryption && !attribute->attcek) ||
+					(!def->encryption && attribute->attcek))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && attribute->attcek)
+				{
+					/*
+					 * Merging the encryption properties of two encrypted
+					 * parent columns is not yet implemented.  Right now, this
+					 * would confuse the checks of the type etc. below (we
+					 * must check the physical and the real types against each
+					 * other, respectively), which might require a larger
+					 * restructuring.  For now, just give up here.
+					 */
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("multiple inheritance of encrypted columns is not implemented")));
+				}
+
+				/*
+				 * Must have the same type, typmod, and collation.
+				 */
 				typenameTypeIdAndMod(NULL, def->typeName, &defTypeId, &deftypmod);
 				if (defTypeId != attribute->atttypid ||
 					deftypmod != attribute->atttypmod)
@@ -2641,6 +2684,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+														attribute->attusertypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -2919,6 +2968,34 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								 errdetail("%s versus %s", def->compression, newdef->compression)));
 				}
 
+				/*
+				 * Check encryption parameter.  All parents and children must
+				 * have the same encryption settings for a column.
+				 */
+				if ((def->encryption && !newdef->encryption) ||
+					(!def->encryption && newdef->encryption))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && newdef->encryption)
+				{
+					FormData_pg_attribute a, newa;
+
+					GetColumnEncryption(def->encryption, &a);
+					GetColumnEncryption(newdef->encryption, &newa);
+
+					if (a.atttypid != newa.atttypid ||
+						a.atttypmod != newa.atttypmod ||
+						a.attcek != newa.attcek)
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" has an encryption specification conflict",
+										attributeName)));
+				}
+
 				/* Mark the column as locally defined */
 				def->is_local = true;
 				/* Merge of NOT NULL constraints = OR 'em together */
@@ -6861,6 +6938,19 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attislocal = colDef->is_local;
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
+	if (colDef->encryption)
+	{
+		GetColumnEncryption(colDef->encryption, &attribute);
+		aclresult = object_aclcheck(ColumnEncKeyRelationId, attribute.attcek, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attribute.attcek, false));
+	}
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attusertypid = 0;
+		attribute.attusertypmod = -1;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12692,6 +12782,9 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
 			case OCLASS_COLLATION:
+			case OCLASS_CEK:
+			case OCLASS_CEKDATA:
+			case OCLASS_CMK:
 			case OCLASS_CONVERSION:
 			case OCLASS_LANGUAGE:
 			case OCLASS_LARGEOBJECT:
@@ -19291,3 +19384,110 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	List	   *cek = NULL;
+	Oid			cekoid;
+	bool		encdet = false;
+	int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+	foreach(lc, coldefencryption)
+	{
+		DefElem    *el = lfirst_node(DefElem, lc);
+
+		if (strcmp(el->defname, "column_encryption_key") == 0)
+			cek = defGetQualifiedName(el);
+		else if (strcmp(el->defname, "encryption_type") == 0)
+		{
+			char	   *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+			if (strcmp(val, "deterministic") == 0)
+				encdet = true;
+			else if (strcmp(val, "randomized") == 0)
+				encdet = false;
+			else
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption type: %s", val));
+		}
+		else if (strcmp(el->defname, "algorithm") == 0)
+		{
+			char	   *val = strVal(el->arg);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption algorithm: %s", val));
+		}
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized column encryption parameter: %s", el->defname));
+	}
+
+	if (!cek)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+				errmsg("column encryption key must be specified"));
+
+	cekoid = get_cek_oid(cek, false);
+
+	attr->attcek = cekoid;
+	attr->attusertypid = attr->atttypid;
+	attr->attusertypmod = attr->atttypmod;
+
+	/* override physical type */
+	if (encdet)
+		attr->atttypid = PG_ENCRYPTED_DETOID;
+	else
+		attr->atttypid = PG_ENCRYPTED_RNDOID;
+	get_typlenbyvalalign(attr->atttypid,
+						 &attr->attlen, &attr->attbyval, &attr->attalign);
+	attr->attstorage = get_typstorage(attr->atttypid);
+	attr->attcollation = InvalidOid;
+
+	attr->atttypmod = alg;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	List	   *result;
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	char	   *nspname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attr->attcek));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attr->attcek);
+
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+	nspname = get_namespace_name(form->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(form->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->atttypmod))),
+									-1));
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index bb0f5de4c2..cec2daa9c3 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -25,6 +25,7 @@
 #include "access/xlogprefetcher.h"
 #include "catalog/pg_authid.h"
 #include "common/string.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "postmaster/postmaster.h"
@@ -706,7 +707,11 @@ check_client_encoding(char **newval, void **extra, GucSource source)
 	 */
 	if (PrepareClientEncoding(encoding) < 0)
 	{
-		if (IsTransactionState())
+		if (MyProcPort->column_encryption_enabled)
+			GUC_check_errdetail("Conversion between %s and %s is not possible when column encryption is enabled.",
+								canonical_name,
+								GetDatabaseEncodingName());
+		else if (IsTransactionState())
 		{
 			/* Must be a genuine no-such-conversion problem */
 			GUC_check_errcode(ERRCODE_FEATURE_NOT_SUPPORTED);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index ff98c773f5..171dca3803 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index dc8415a693..c81bc15128 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3983,6 +3983,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(coldef->compression))
 					return true;
+				if (WALK(coldef->encryption))
+					return true;
 				if (WALK(coldef->raw_default))
 					return true;
 				if (WALK(coldef->collClause))
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a0138382a1..a6039878fa 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,6 +420,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <list>	parse_toplevel stmtmulti routine_body_stmt_list
 				OptTableElementList TableElementList OptInherit definition
+				list_of_definitions
 				OptTypedTableElementList TypedTableElementList
 				reloptions opt_reloptions
 				OptWith opt_definition func_args func_args_list
@@ -592,6 +594,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableConstraint TableLikeClause
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression column_storage opt_column_storage
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_match
@@ -690,8 +693,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
 	DOUBLE_P DROP
 
-	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
-	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EACH ELSE ENABLE_P ENCODING ENCRYPTION ENCRYPTED END_P ENUM_P ESCAPE
+	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -708,13 +711,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	JOIN
 
-	KEY
+	KEY KEYS
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
-	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+	MAPPING MASTER MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
 
 	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -1988,7 +1993,7 @@ CheckPointStmt:
 
 /*****************************************************************************
  *
- * DISCARD { ALL | TEMP | PLANS | SEQUENCES }
+ * DISCARD
  *
  *****************************************************************************/
 
@@ -2014,6 +2019,13 @@ DiscardStmt:
 					n->target = DISCARD_TEMP;
 					$$ = (Node *) n;
 				}
+			| DISCARD COLUMN ENCRYPTION KEYS
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+
+					n->target = DISCARD_COLUMN_ENCRYPTION_KEYS;
+					$$ = (Node *) n;
+				}
 			| DISCARD PLANS
 				{
 					DiscardStmt *n = makeNode(DiscardStmt);
@@ -3700,14 +3712,15 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_storage opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_encryption opt_column_storage opt_column_compression create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 
 					n->colname = $1;
 					n->typeName = $2;
-					n->storage_name = $3;
-					n->compression = $4;
+					n->encryption = $3;
+					n->storage_name = $4;
+					n->compression = $5;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3716,8 +3729,8 @@ columnDef:	ColId Typename opt_column_storage opt_column_compression create_gener
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $5;
-					SplitColQualList($6, &n->constraints, &n->collClause,
+					n->fdwoptions = $6;
+					SplitColQualList($7, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *) n;
@@ -3774,6 +3787,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4052,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6289,33 @@ DefineStmt:
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+			| CREATE COLUMN ENCRYPTION KEY any_name WITH VALUES list_of_definitions
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CEK;
+					n->defnames = $5;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name WITH definition
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = $7;
+					$$ = (Node *) n;
+				}
 		;
 
 definition: '(' def_list ')'						{ $$ = $2; }
@@ -6289,6 +6335,10 @@ def_elem:	ColLabel '=' def_arg
 				}
 		;
 
+list_of_definitions: definition						{ $$ = list_make1($1); }
+			| list_of_definitions ',' definition	{ $$ = lappend($1, $3); }
+		;
+
 /* Note: any simple identifier will be returned as a type name! */
 def_arg:	func_type						{ $$ = (Node *) $1; }
 			| reserved_keyword				{ $$ = (Node *) makeString(pstrdup($1)); }
@@ -6800,6 +6850,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -7611,6 +7663,24 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| COLUMN ENCRYPTION KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CEK;
+					n->objs = $4;
+					$$ = n;
+				}
+			| COLUMN MASTER KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CMK;
+					n->objs = $4;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9140,6 +9210,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9817,6 +9907,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10148,6 +10258,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11304,6 +11432,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_name ADD_P VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = false;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN ENCRYPTION KEY any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16965,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16840,6 +17015,7 @@ unreserved_keyword:
 			| INVOKER
 			| ISOLATION
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -16854,6 +17030,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17514,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17404,6 +17582,7 @@ bare_label_keyword:
 			| ISOLATION
 			| JOIN
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -17425,6 +17604,7 @@ bare_label_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index 2240284f21..a8a1855aa0 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,159 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			Expr	   *expr = te->expr;
+
+			/*
+			 * If it's a RelabelType, look inside.  (For encrypted columns,
+			 * this would typically be a typmod adjustment.)
+			 */
+			if (IsA(expr, RelabelType))
+				expr = castNode(RelabelType, expr)->arg;
+
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(expr, Param))
+			{
+				Param	   *p = (Param *) expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(expr, Var))
+			{
+				Var		   *v = (Var *) expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell   *lc;
+
+					foreach(lc, srcrte->values_lists)
+					{
+						List	   *values_list = lfirst_node(List, lc);
+						Expr	   *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, RelabelType))
+							value = castNode(RelabelType, value)->arg;
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach(lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f9218f48aa..8d902292cd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1035,8 +1035,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+												attribute->attusertypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2552327d90..79ec9c4d1f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2210,12 +2210,27 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 									valptr),
 							 errhint("Valid values are: \"false\", 0, \"true\", 1, \"database\".")));
 			}
+			else if (strcmp(nameptr, "_pq_.column_encryption") == 0)
+			{
+				/*
+				 * Right now, the only accepted value is "1".  This gives room
+				 * to expand this into a version number, for example.
+				 */
+				if (strcmp(valptr, "1") == 0)
+					port->column_encryption_enabled = true;
+				else
+					ereport(FATAL,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for parameter \"%s\": \"%s\"",
+									"column_encryption",
+									valptr),
+							 errhint("Valid values are: 1.")));
+			}
 			else if (strncmp(nameptr, "_pq_.", 5) == 0)
 			{
 				/*
 				 * Any option beginning with _pq_. is reserved for use as a
-				 * protocol-level option, but at present no such options are
-				 * defined.
+				 * protocol-level option.
 				 */
 				unrecognized_protocol_options =
 					lappend(unrecognized_protocol_options, pstrdup(nameptr));
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index cab709b07b..b577557384 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -71,6 +72,7 @@
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
 
@@ -1815,6 +1817,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (type_is_encrypted(ptype))
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter $%d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2560,6 +2572,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2618,11 +2632,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	for (int i = 0; i < psrc->num_params; i++)
 	{
 		Oid			ptype = psrc->param_types[i];
+		Oid			pcekid = InvalidOid;
+		int			pcekalg = 0;
+		int16		pflags = 0;
+
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(ptype))
+		{
+			Oid			porigtbl = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("parameter $%d corresponds to an encrypted column, but an underlying table and column could not be determined", i + 1)));
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(porigtbl), Int16GetDatum(porigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", porigcol, porigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			ptype = orig_att->attusertypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x0001;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&row_description_buf, (int) pcekid);
+			pq_sendint32(&row_description_buf, pcekalg);
+			pq_sendint16(&row_description_buf, pflags);
+		}
 	}
 	pq_endmessage_reuse(&row_description_buf);
 
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index c7d9d96b45..180f86cb79 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -30,6 +30,7 @@
 #include "commands/alter.h"
 #include "commands/async.h"
 #include "commands/cluster.h"
+#include "commands/colenccmds.h"
 #include "commands/collationcmds.h"
 #include "commands/comment.h"
 #include "commands/conversioncmds.h"
@@ -137,6 +138,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,14 @@ ProcessUtilitySlow(ParseState *pstate,
 															stmt->definition,
 															&secondaryObject);
 							break;
+						case OBJECT_CEK:
+							Assert(stmt->args == NIL);
+							address = CreateCEK(pstate, stmt);
+							break;
+						case OBJECT_CMK:
+							Assert(stmt->args == NIL);
+							address = CreateCMK(pstate, stmt);
+							break;
 						case OBJECT_COLLATION:
 							Assert(stmt->args == NIL);
 							address = DefineCollation(pstate,
@@ -1903,6 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,12 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_COLUMN:
 			tag = CMDTAG_ALTER_TABLE;
 			break;
+		case OBJECT_CEK:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+		case OBJECT_CMK:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
 		case OBJECT_CONVERSION:
 			tag = CMDTAG_ALTER_CONVERSION;
 			break;
@@ -2640,6 +2665,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_DROP_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_DROP_COLUMN_MASTER_KEY;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2760,6 +2791,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_COLLATION:
 					tag = CMDTAG_CREATE_COLLATION;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_CREATE_COLUMN_MASTER_KEY;
+					break;
 				case OBJECT_ACCESS_METHOD:
 					tag = CMDTAG_CREATE_ACCESS_METHOD;
 					break;
@@ -2917,6 +2954,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_ALL:
 					tag = CMDTAG_DISCARD_ALL;
 					break;
+				case DISCARD_COLUMN_ENCRYPTION_KEYS:
+					tag = CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS;
+					break;
 				case DISCARD_PLANS:
 					tag = CMDTAG_DISCARD_PLANS;
 					break;
@@ -3063,6 +3103,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3736,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 8f7522d103..6aeb06f8fd 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -22,6 +22,8 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_foreign_data_wrapper.h"
 #include "catalog/pg_foreign_server.h"
@@ -101,6 +103,10 @@ static AclMode convert_table_priv_string(text *priv_type_text);
 static AclMode convert_sequence_priv_string(text *priv_type_text);
 static AttrNumber convert_column_name(Oid tableoid, text *column);
 static AclMode convert_column_priv_string(text *priv_type_text);
+static Oid	convert_column_encryption_key_name(text *cekname);
+static AclMode convert_column_encryption_key_priv_string(text *priv_type_text);
+static Oid	convert_column_master_key_name(text *cmkname);
+static AclMode convert_column_master_key_priv_string(text *priv_type_text);
 static Oid	convert_database_name(text *databasename);
 static AclMode convert_database_priv_string(text *priv_type_text);
 static Oid	convert_foreign_data_wrapper_name(text *fdwname);
@@ -800,6 +806,14 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			/* for backwards compatibility, grant some rights by default */
 			world_default = ACL_CREATE_TEMP | ACL_CONNECT;
@@ -911,6 +925,12 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 's':
 			objtype = OBJECT_SEQUENCE;
 			break;
+		case 'Y':
+			objtype = OBJECT_CEK;
+			break;
+		case 'y':
+			objtype = OBJECT_CMK;
+			break;
 		case 'd':
 			objtype = OBJECT_DATABASE;
 			break;
@@ -2915,6 +2935,384 @@ convert_column_priv_string(text *priv_type_text)
 }
 
 
+/*
+ * has_column_encryption_key_privilege variants
+ *		These are all named "has_column_encryption_key_privilege" at the SQL level.
+ *		They take various combinations of column encryption key name,
+ *		cek OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_encryption_key_privilege_name_name
+ *		Check user privileges on a column encryption key given
+ *		name username, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name
+ *		Check user privileges on a column encryption key given
+ *		text cekname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cekname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name_id
+ *		Check user privileges on a column encryption key given
+ *		name usename, column encryption key oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id
+ *		Check user privileges on a column encryption key given
+ *		column encryption key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cekid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_name
+ *		Check user privileges on a column encryption key given
+ *		roleid, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_id
+ *		Check user privileges on a column encryption key given
+ *		roleid, cek oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_encryption_key_privilege family.
+ */
+
+/*
+ * Given a CEK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_encryption_key_name(text *cekname)
+{
+	return get_cek_oid(textToQualifiedNameList(cekname), false);
+}
+
+/*
+ * convert_column_encryption_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_encryption_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_encryption_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_encryption_key_priv_map);
+}
+
+
+/*
+ * has_column_master_key_privilege variants
+ *		These are all named "has_column_master_key_privilege" at the SQL level.
+ *		They take various combinations of column master key name,
+ *		cmk OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_master_key_privilege_name_name
+ *		Check user privileges on a column master key given
+ *		name username, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name
+ *		Check user privileges on a column master key given
+ *		text cmkname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cmkname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name_id
+ *		Check user privileges on a column master key given
+ *		name usename, column master key oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id
+ *		Check user privileges on a column master key given
+ *		column master key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cmkid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_name
+ *		Check user privileges on a column master key given
+ *		roleid, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_id
+ *		Check user privileges on a column master key given
+ *		roleid, cmk oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_master_key_privilege family.
+ */
+
+/*
+ * Given a CMK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_master_key_name(text *cmkname)
+{
+	return get_cmk_oid(textToQualifiedNameList(cmkname), false);
+}
+
+/*
+ * convert_column_master_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_master_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_master_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_master_key_priv_map);
+}
+
+
 /*
  * has_database_privilege variants
  *		These are all named "has_database_privilege" at the SQL level.
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 5778e3f0ef..dd21fcf59e 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -679,6 +679,113 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	Node	   *escontext = fcinfo->context;
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c07382051d..7ad159110f 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -24,7 +24,10 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_namespace.h"
@@ -2658,6 +2661,25 @@ type_is_multirange(Oid typid)
 	return (get_typtype(typid) == TYPTYPE_MULTIRANGE);
 }
 
+bool
+type_is_encrypted(Oid typid)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+		bool		result;
+
+		result = (typtup->typcategory == TYPCATEGORY_ENCRYPTED);
+		ReleaseSysCache(tp);
+		return result;
+	}
+	else
+		return false;
+}
+
 /*
  * get_type_category_preferred
  *
@@ -3683,3 +3705,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+char *
+get_cek_name(Oid cekid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cekname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+		return NULL;
+	}
+
+	cekname = pstrdup(NameStr(((Form_pg_colenckey) GETSTRUCT(tup))->cekname));
+
+	ReleaseSysCache(tup);
+
+	return cekname;
+}
+
+Oid
+get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok)
+{
+	Oid			cekdataid;
+
+	cekdataid = GetSysCacheOid2(CEKDATACEKCMK, Anum_pg_colenckeydata_oid,
+								ObjectIdGetDatum(cekid),
+								ObjectIdGetDatum(cmkid));
+	if (!OidIsValid(cekdataid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" has no data for master key \"%s\"",
+						get_cek_name(cekid, false), get_cmk_name(cmkid, false))));
+
+	return cekdataid;
+}
+
+char *
+get_cmk_name(Oid cmkid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cmkname;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+		return NULL;
+	}
+
+	cmkname = pstrdup(NameStr(((Form_pg_colmasterkey) GETSTRUCT(tup))->cmkname));
+
+	ReleaseSysCache(tup);
+
+	return cmkname;
+}
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 77c2ba3f8f..d40af13efe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 94abede512..eb432260da 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -219,6 +222,32 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAMENSP] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameNspIndexId,
+		KEY(Anum_pg_colenckey_cekname,
+			Anum_pg_colenckey_ceknamespace),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +262,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAMENSP] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameNspIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname,
+			Anum_pg_colmasterkey_cmknamespace),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 033647011b..4b6be68f27 100644
--- a/src/backend/utils/mb/mbutils.c
+++ b/src/backend/utils/mb/mbutils.c
@@ -36,7 +36,9 @@
 
 #include "access/xact.h"
 #include "catalog/namespace.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
@@ -130,6 +132,12 @@ PrepareClientEncoding(int encoding)
 		encoding == PG_SQL_ASCII)
 		return 0;
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	if (IsTransactionState())
 	{
 		/*
@@ -237,6 +245,12 @@ SetClientEncoding(int encoding)
 		return 0;
 	}
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	/*
 	 * Search the cache for the entry previously prepared by
 	 * PrepareClientEncoding; if there isn't one, we lose.  While at it,
@@ -297,7 +311,9 @@ InitializeClientEncoding(void)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("conversion between %s and %s is not supported",
 						pg_enc2name_tbl[pending_client_encoding].name,
-						GetDatabaseEncodingName())));
+						GetDatabaseEncodingName()),
+				 (MyProcPort->column_encryption_enabled) ?
+				 errdetail("Encoding conversion is not possible when column encryption is enabled.") : 0));
 	}
 
 	/*
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index a43f2e5553..e67462c81a 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -201,6 +203,12 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading user-defined collations");
 	(void) getCollations(fout, &numCollations);
 
+	pg_log_info("reading column master keys");
+	getColumnMasterKeys(fout);
+
+	pg_log_info("reading column encryption keys");
+	getColumnEncryptionKeys(fout);
+
 	pg_log_info("reading user-defined conversions");
 	getConversions(fout, &numConversions);
 
@@ -859,6 +867,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 9753a6d868..0a5178bcf1 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -484,6 +484,10 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('U', "USAGE");
 	}
+	else if (strcmp(type, "COLUMN ENCRYPTION KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "COLUMN MASTER KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "DATABASE") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,7 @@ typedef struct _connParams
 	char	   *pghost;
 	char	   *username;
 	trivalue	promptPassword;
+	int			column_encryption;
 	/* If not NULL, this overrides the dbname obtained from command line */
 	/* (but *only* the DB name, not anything else in the connstring) */
 	char	   *override_dbname;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 61ebb8fe85..bc303550fd 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3396,6 +3396,8 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 
 	/* objects that don't require special decoration */
 	if (strcmp(type, "COLLATION") == 0 ||
+		strcmp(type, "COLUMN ENCRYPTION KEY") == 0 ||
+		strcmp(type, "COLUMN MASTER KEY") == 0 ||
 		strcmp(type, "CONVERSION") == 0 ||
 		strcmp(type, "DOMAIN") == 0 ||
 		strcmp(type, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 100644
--- a/src/bin/pg_dump/pg_backup_db.c
+++ b/src/bin/pg_dump/pg_backup_db.c
@@ -133,8 +133,8 @@ ConnectDatabase(Archive *AHX,
 	 */
 	do
 	{
-		const char *keywords[8];
-		const char *values[8];
+		const char *keywords[9];
+		const char *values[9];
 		int			i = 0;
 
 		/*
@@ -159,6 +159,11 @@ ConnectDatabase(Archive *AHX,
 		}
 		keywords[i] = "fallback_application_name";
 		values[i++] = progname;
+		if (cparams->column_encryption)
+		{
+			keywords[i] = "column_encryption";
+			values[i++] = "1";
+		}
 		keywords[i] = NULL;
 		values[i++] = NULL;
 		Assert(i <= lengthof(keywords));
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 24ba936332..8c61fcab13 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,8 @@ static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
 static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
 static void dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo);
 static void dumpCollation(Archive *fout, const CollInfo *collinfo);
+static void dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo);
+static void dumpColumnMasterKey(Archive *fout, const CmkInfo *cekinfo);
 static void dumpConversion(Archive *fout, const ConvInfo *convinfo);
 static void dumpRule(Archive *fout, const RuleInfo *rinfo);
 static void dumpAgg(Archive *fout, const AggInfo *agginfo);
@@ -393,6 +396,7 @@ main(int argc, char **argv)
 		{"attribute-inserts", no_argument, &dopt.column_inserts, 1},
 		{"binary-upgrade", no_argument, &dopt.binary_upgrade, 1},
 		{"column-inserts", no_argument, &dopt.column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &dopt.cparams.column_encryption, 1},
 		{"disable-dollar-quoting", no_argument, &dopt.disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &dopt.disable_triggers, 1},
 		{"enable-row-security", no_argument, &dopt.enable_row_security, 1},
@@ -685,6 +689,9 @@ main(int argc, char **argv)
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
 	 */
+	if (dopt.cparams.column_encryption && dopt.dump_inserts == 0)
+		pg_fatal("option --decrypt-encrypted-columns requires option --inserts, --rows-per-insert, or --column-inserts");
+
 	if (dopt.do_nothing && dopt.dump_inserts == 0)
 		pg_fatal("option --on-conflict-do-nothing requires option --inserts, --rows-per-insert, or --column-inserts");
 
@@ -1056,6 +1063,7 @@ help(const char *progname)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security (dump only content user has\n"
@@ -5571,6 +5579,164 @@ getCollations(Archive *fout, int *numCollations)
 	return collinfo;
 }
 
+/*
+ * getColumnEncryptionKeys
+ *	  get information about column encryption keys
+ */
+void
+getColumnEncryptionKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CekInfo	   *cekinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cekname;
+	int			i_ceknamespace;
+	int			i_cekowner;
+	int			i_cekacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, cek.cekowner, cek.cekacl, acldefault('Y', cek.cekowner) AS acldefault\n"
+					  "FROM pg_colenckey cek");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cekname = PQfnumber(res, "cekname");
+	i_ceknamespace = PQfnumber(res, "ceknamespace");
+	i_cekowner = PQfnumber(res, "cekowner");
+	i_cekacl = PQfnumber(res, "cekacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cekinfo = pg_malloc(ntups * sizeof(CekInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		PGresult	   *res2;
+		int				ntups2;
+
+		cekinfo[i].dobj.objType = DO_CEK;
+		cekinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cekinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cekinfo[i].dobj);
+		cekinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cekname));
+		cekinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cekacl));
+		cekinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cekinfo[i].dacl.privtype = 0;
+		cekinfo[i].dacl.initprivs = NULL;
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, ckdcmkalg, ckdencval\n"
+						  "FROM pg_catalog.pg_colenckeydata\n"
+						  "WHERE ckdcekid = %u", cekinfo[i].dobj.catId.oid);
+		res2 = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		ntups2 = PQntuples(res2);
+		cekinfo[i].numdata = ntups2;
+		cekinfo[i].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			cekinfo[i].cekcmkalgs[j] = atoi(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkalg")));
+			cekinfo[i].cekencvals[j] = pg_strdup(PQgetvalue(res2, j, PQfnumber(res2, "ckdencval")));
+		}
+		PQclear(res2);
+
+		selectDumpableObject(&(cekinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cekacl))
+			cekinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * getColumnMasterKeys
+ *	  get information about column master keys
+ */
+void
+getColumnMasterKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CmkInfo	   *cmkinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cmkname;
+	int			i_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+	int			i_cmkacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, cmk.cmkowner, cmk.cmkrealm, cmk.cmkacl, acldefault('y', cmk.cmkowner) AS acldefault\n"
+					  "FROM pg_colmasterkey cmk");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cmkname = PQfnumber(res, "cmkname");
+	i_cmknamespace = PQfnumber(res, "cmknamespace");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+	i_cmkacl = PQfnumber(res, "cmkacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cmkinfo = pg_malloc(ntups * sizeof(CmkInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		cmkinfo[i].dobj.objType = DO_CMK;
+		cmkinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cmkinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cmkinfo[i].dobj);
+		cmkinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cmkname));
+		cmkinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		cmkinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cmkacl));
+		cmkinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cmkinfo[i].dacl.privtype = 0;
+		cmkinfo[i].dacl.initprivs = NULL;
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cmkacl))
+			cmkinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8186,6 +8352,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8245,8 +8414,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
+	appendPQExpBuffer(q, "SELECT\n"
 						 "a.attrelid,\n"
 						 "a.attnum,\n"
 						 "a.attname,\n"
@@ -8259,7 +8427,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attlen,\n"
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
+						 "pg_catalog.format_type(%s) AS atttypname,\n"
 						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
@@ -8268,7 +8436,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "' ' || pg_catalog.quote_literal(option_value) "
 						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
 						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+						 "), E',\n    ') AS attfdwoptions,\n",
+						 fout->remoteVersion >= 160000 ?
+						 "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END, CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END" :
+						 "a.atttypid, a.atttypmod");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -8294,10 +8465,23 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	if (fout->remoteVersion >= 120000)
 		appendPQExpBufferStr(q,
-							 "a.attgenerated\n");
+							 "a.attgenerated,\n");
+	else
+		appendPQExpBufferStr(q,
+							 "'' AS attgenerated,\n");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBuffer(q,
+						  "a.attcek,\n"
+						  "CASE a.atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "CASE WHEN a.atttypid IN (%u, %u) THEN a.atttypmod END AS attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID,
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcek,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
@@ -8322,6 +8506,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	i_attencdet = PQfnumber(res, "attencdet");
+	i_attencalg = PQfnumber(res, "attencalg");
 	i_attisdropped = PQfnumber(res, "attisdropped");
 	i_attlen = PQfnumber(res, "attlen");
 	i_attalign = PQfnumber(res, "attalign");
@@ -8382,6 +8569,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		tbinfo->typstorage = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attidentity = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attgenerated = (char *) pg_malloc(numatts * sizeof(char));
+		tbinfo->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		tbinfo->attencdet = (bool *) pg_malloc(numatts * sizeof(bool));
+		tbinfo->attencalg = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char));
@@ -8409,6 +8599,22 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->attidentity[j] = *(PQgetvalue(res, r, i_attidentity));
 			tbinfo->attgenerated[j] = *(PQgetvalue(res, r, i_attgenerated));
 			tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
+			if (!PQgetisnull(res, r, i_attcek))
+			{
+				Oid		attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[j] = NULL;
+			if (!PQgetisnull(res, r, i_attencdet))
+				tbinfo->attencdet[j] = (PQgetvalue(res, r, i_attencdet)[0] == 't');
+			else
+				tbinfo->attencdet[j] = 0;
+			if (!PQgetisnull(res, r, i_attencalg))
+				tbinfo->attencalg[j] = atoi(PQgetvalue(res, r, i_attencalg));
+			else
+				tbinfo->attencalg[j] = 0;
 			tbinfo->attisdropped[j] = (PQgetvalue(res, r, i_attisdropped)[0] == 't');
 			tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
 			tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
@@ -9927,6 +10133,12 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_OPFAMILY:
 			dumpOpfamily(fout, (const OpfamilyInfo *) dobj);
 			break;
+		case DO_CEK:
+			dumpColumnEncryptionKey(fout, (const CekInfo *) dobj);
+			break;
+		case DO_CMK:
+			dumpColumnMasterKey(fout, (const CmkInfo *) dobj);
+			break;
 		case DO_COLLATION:
 			dumpCollation(fout, (const CollInfo *) dobj);
 			break;
@@ -13320,6 +13532,141 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * dumpColumnEncryptionKey
+ *	  dump the definition of the given column encryption key
+ */
+static void
+dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcekname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcekname = pg_strdup(fmtId(cekinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN ENCRYPTION KEY %s;\n",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_name(cekinfo->cekcmkalgs[i]));
+		appendPQExpBuffer(query, "encrypted_value = ");
+		appendStringLiteralAH(query, cekinfo->cekencvals[i], fout);
+
+		appendPQExpBuffer(query, ")");
+		if (i < cekinfo->numdata - 1)
+			appendPQExpBuffer(query, ", ");
+	}
+
+	appendPQExpBufferStr(query, ";\n");
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cekinfo->dobj.catId, cekinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cekinfo->dobj.name,
+								  .namespace = cekinfo->dobj.namespace->dobj.name,
+								  .owner = cekinfo->rolname,
+								  .description = "COLUMN ENCRYPTION KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cekinfo->dobj.dumpId, InvalidDumpId, "COLUMN ENCRYPTION KEY",
+				qcekname, NULL, cekinfo->dobj.namespace->dobj.name,
+				cekinfo->rolname, &cekinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcekname);
+}
+
+/*
+ * dumpColumnMasterKey
+ *	  dump the definition of the given column master key
+ */
+static void
+dumpColumnMasterKey(Archive *fout, const CmkInfo *cmkinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcmkname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcmkname = pg_strdup(fmtId(cmkinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN MASTER KEY %s;\n",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "realm = ");
+	appendStringLiteralAH(query, cmkinfo->cmkrealm, fout);
+
+	appendPQExpBufferStr(query, ");\n");
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cmkinfo->dobj.catId, cmkinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cmkinfo->dobj.name,
+								  .namespace = cmkinfo->dobj.namespace->dobj.name,
+								  .owner = cmkinfo->rolname,
+								  .description = "COLUMN MASTER KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN MASTER KEY", qcmkname,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cmkinfo->dobj.dumpId, InvalidDumpId, "COLUMN MASTER KEY",
+				qcmkname, NULL, cmkinfo->dobj.namespace->dobj.name,
+				cmkinfo->rolname, &cmkinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15403,6 +15750,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[j]));
+						/*
+						 * To reduce output size, we don't print the default
+						 * of encryption_type, but we do print the default of
+						 * algorithm, since we might want to change to a new
+						 * default algorithm sometime in the future.
+						 */
+						if (tbinfo->attencdet[j])
+							appendPQExpBuffer(q, "encryption_type = deterministic, ");
+						appendPQExpBuffer(q, "algorithm = '%s')",
+										  get_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -17971,6 +18334,8 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_ACCESS_METHOD:
 			case DO_OPCLASS:
 			case DO_OPFAMILY:
+			case DO_CEK:
+			case DO_CMK:
 			case DO_COLLATION:
 			case DO_CONVERSION:
 			case DO_TABLE:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index cdca0b993d..d4a2e595d0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -47,6 +47,8 @@ typedef enum
 	DO_ACCESS_METHOD,
 	DO_OPCLASS,
 	DO_OPFAMILY,
+	DO_CEK,
+	DO_CMK,
 	DO_COLLATION,
 	DO_CONVERSION,
 	DO_TABLE,
@@ -332,6 +334,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	struct _CekInfo **attcek;
+	int		   *attencalg;
+	bool	   *attencdet;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
 	char	   *attalign;		/* attribute align, used by binary_upgrade */
 	bool	   *attislocal;		/* true if attr has local definition */
@@ -663,6 +668,32 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	struct _CmkInfo **cekcmks;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -683,6 +714,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -710,6 +743,8 @@ extern AccessMethodInfo *getAccessMethods(Archive *fout, int *numAccessMethods);
 extern OpclassInfo *getOpclasses(Archive *fout, int *numOpclasses);
 extern OpfamilyInfo *getOpfamilies(Archive *fout, int *numOpfamilies);
 extern CollInfo *getCollations(Archive *fout, int *numCollations);
+extern void getColumnEncryptionKeys(Archive *fout);
+extern void getColumnMasterKeys(Archive *fout);
 extern ConvInfo *getConversions(Archive *fout, int *numConversions);
 extern TableInfo *getTables(Archive *fout, int *numTables);
 extern void getOwnedSeqs(Archive *fout, TableInfo tblinfo[], int numTables);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 8266c117a3..d3dacd39da 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -69,6 +69,8 @@ enum dbObjectTypePriorities
 	PRIO_TSTEMPLATE,
 	PRIO_TSDICT,
 	PRIO_TSCONFIG,
+	PRIO_CMK,
+	PRIO_CEK,
 	PRIO_FDW,
 	PRIO_FOREIGN_SERVER,
 	PRIO_TABLE,
@@ -111,6 +113,8 @@ static const int dbObjectTypePriority[] =
 	PRIO_ACCESS_METHOD,			/* DO_ACCESS_METHOD */
 	PRIO_OPFAMILY,				/* DO_OPCLASS */
 	PRIO_OPFAMILY,				/* DO_OPFAMILY */
+	PRIO_CEK,					/* DO_CEK */
+	PRIO_CMK,					/* DO_CMK */
 	PRIO_COLLATION,				/* DO_COLLATION */
 	PRIO_CONVERSION,			/* DO_CONVERSION */
 	PRIO_TABLE,					/* DO_TABLE */
@@ -1322,6 +1326,16 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "OPERATOR FAMILY %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_CEK:
+			snprintf(buf, bufsize,
+					 "COLUMN ENCRYPTION KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
+		case DO_CMK:
+			snprintf(buf, bufsize,
+					 "COLUMN MASTER KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_COLLATION:
 			snprintf(buf, bufsize,
 					 "COLLATION %s  (ID %d OID %u)",
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index cd421c5944..6530fb81a2 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -93,6 +93,7 @@ static bool dosync = true;
 
 static int	binary_upgrade = 0;
 static int	column_inserts = 0;
+static int	decrypt_encrypted_columns = 0;
 static int	disable_dollar_quoting = 0;
 static int	disable_triggers = 0;
 static int	if_exists = 0;
@@ -154,6 +155,7 @@ main(int argc, char *argv[])
 		{"attribute-inserts", no_argument, &column_inserts, 1},
 		{"binary-upgrade", no_argument, &binary_upgrade, 1},
 		{"column-inserts", no_argument, &column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &decrypt_encrypted_columns, 1},
 		{"disable-dollar-quoting", no_argument, &disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &disable_triggers, 1},
 		{"exclude-database", required_argument, NULL, 6},
@@ -424,6 +426,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --binary-upgrade");
 	if (column_inserts)
 		appendPQExpBufferStr(pgdumpopts, " --column-inserts");
+	if (decrypt_encrypted_columns)
+		appendPQExpBufferStr(pgdumpopts, " --decrypt-encrypted-columns");
 	if (disable_dollar_quoting)
 		appendPQExpBufferStr(pgdumpopts, " --disable-dollar-quoting");
 	if (disable_triggers)
@@ -649,6 +653,7 @@ help(void)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --exclude-database=PATTERN   exclude databases whose name matches PATTERN\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 72b19ee6cd..837d515639 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -719,6 +719,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
 	'ALTER FOREIGN DATA WRAPPER dummy OWNER TO' => {
 		regexp => qr/^ALTER FOREIGN DATA WRAPPER dummy OWNER TO .+;/m,
 		like   => { %full_runs, section_pre_data => 1, },
@@ -1319,6 +1331,26 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1737,6 +1769,26 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3570,6 +3622,26 @@
 		unlike => { no_privs => 1, },
 	},
 
+	'GRANT USAGE ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
+	'GRANT USAGE ON COLUMN MASTER KEY cmk1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
 	'GRANT USAGE ON FOREIGN DATA WRAPPER dummy' => {
 		create_order => 85,
 		create_sql   => 'GRANT USAGE ON FOREIGN DATA WRAPPER dummy
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..0d6a46d24b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c8a0bb7b3a..04d437f836 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1530,7 +1530,7 @@ describeOneTableDetails(const char *schemaname,
 	bool		printTableInitialized = false;
 	int			i;
 	char	   *view_def = NULL;
-	char	   *headers[12];
+	char	   *headers[13];
 	PQExpBufferData title;
 	PQExpBufferData tmpbuf;
 	int			cols;
@@ -1546,6 +1546,7 @@ describeOneTableDetails(const char *schemaname,
 				fdwopts_col = -1,
 				attstorage_col = -1,
 				attcompression_col = -1,
+				attcekname_col = -1,
 				attstattarget_col = -1,
 				attdescr_col = -1;
 	int			numrows;
@@ -1568,6 +1569,8 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attusertypid;
+	const char *attusertypmod;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1844,7 +1847,17 @@ describeOneTableDetails(const char *schemaname,
 	cols = 0;
 	printfPQExpBuffer(&buf, "SELECT a.attname");
 	attname_col = cols++;
-	appendPQExpBufferStr(&buf, ",\n  pg_catalog.format_type(a.atttypid, a.atttypmod)");
+	if (pset.sversion >= 160000)
+	{
+		attusertypid = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END";
+		attusertypmod = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END";
+	}
+	else
+	{
+		attusertypid = "a.atttypid";
+		attusertypmod = "a.atttypmod";
+	}
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, %s)", attusertypid, attusertypmod);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1857,7 +1870,8 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		attrdef_col = cols++;
 		attnotnull_col = cols++;
-		appendPQExpBufferStr(&buf, ",\n  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
+		appendPQExpBufferStr(&buf, ",\n"
+							 "  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
 							 "   WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) AS attcollation");
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
@@ -1909,6 +1923,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			appendPQExpBufferStr(&buf, ",\n  (SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek) AS attcekname");
+			attcekname_col = cols++;
+		}
+
 		/* stats target, if relevant to relkind */
 		if (tableinfo.relkind == RELKIND_RELATION ||
 			tableinfo.relkind == RELKIND_INDEX ||
@@ -2032,6 +2058,8 @@ describeOneTableDetails(const char *schemaname,
 		headers[cols++] = gettext_noop("Storage");
 	if (attcompression_col >= 0)
 		headers[cols++] = gettext_noop("Compression");
+	if (attcekname_col >= 0)
+		headers[cols++] = gettext_noop("Encryption");
 	if (attstattarget_col >= 0)
 		headers[cols++] = gettext_noop("Stats target");
 	if (attdescr_col >= 0)
@@ -2124,6 +2152,17 @@ describeOneTableDetails(const char *schemaname,
 							  false, false);
 		}
 
+		/* Column encryption */
+		if (attcekname_col >= 0)
+		{
+			if (!PQgetisnull(res, i, attcekname_col))
+				printTableAddCell(&cont, PQgetvalue(res, i, attcekname_col),
+								  false, false);
+			else
+				printTableAddCell(&cont, "",
+								  false, false);
+		}
+
 		/* Statistics target, if the relkind supports this feature */
 		if (attstattarget_col >= 0)
 			printTableAddCell(&cont, PQgetvalue(res, i, attstattarget_col),
@@ -4477,6 +4516,152 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cekacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cmkacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cmk.oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 554fe86725..1cf8f72176 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1729966959 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,8 @@ helpVariables(unsigned short int pager)
 		  "    true if last query failed, else false\n");
 	HELP0("  FETCH_COUNT\n"
 		  "    the number of result rows to fetch and display at a time (0 = unlimited)\n");
+	HELP0("  HIDE_COLUMN_ENCRYPTION\n"
+		  "    if set, column encryption details are not displayed\n");
 	HELP0("  HIDE_TABLEAM\n"
 		  "    if set, table access methods are not displayed\n");
 	HELP0("  HIDE_TOAST_COMPRESSION\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 73d4b393bc..010bc5a6d5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,7 @@ typedef struct _psqlSettings
 	bool		quiet;
 	bool		singleline;
 	bool		singlestep;
+	bool		hide_column_encryption;
 	bool		hide_compression;
 	bool		hide_tableam;
 	int			fetch_count;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 5a28b6f713..6736505c3a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -1188,6 +1188,13 @@ hide_compression_hook(const char *newval)
 							 &pset.hide_compression);
 }
 
+static bool
+hide_column_encryption_hook(const char *newval)
+{
+	return ParseVariableBool(newval, "HIDE_COLUMN_ENCRYPTION",
+							 &pset.hide_column_encryption);
+}
+
 static bool
 hide_tableam_hook(const char *newval)
 {
@@ -1259,6 +1266,9 @@ EstablishVariableSpace(void)
 	SetVariableHooks(pset.vars, "SHOW_CONTEXT",
 					 show_context_substitute_hook,
 					 show_context_hook);
+	SetVariableHooks(pset.vars, "HIDE_COLUMN_ENCRYPTION",
+					 bool_substitute_hook,
+					 hide_column_encryption_hook);
 	SetVariableHooks(pset.vars, "HIDE_TOAST_COMPRESSION",
 					 bool_substitute_hook,
 					 hide_compression_hook);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5e1882eaea..0642449fa0 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -933,6 +933,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1223,6 +1237,8 @@ static const pgsql_thing_t words_after_create[] = {
 	{"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", NULL, NULL, &Query_for_list_of_collations},
+	{"COLUMN ENCRYPTION KEY", NULL, NULL, NULL},
+	{"COLUMN MASTER KEY KEY", NULL, NULL, NULL},
 
 	/*
 	 * CREATE CONSTRAINT TRIGGER is not supported here because it is designed
@@ -1705,7 +1721,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1952,6 +1968,22 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "COLLATION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "REFRESH VERSION", "RENAME TO", "SET SCHEMA");
 
+	/* ALTER/DROP COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "ENCRYPTION", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_ceks);
+
+	/* ALTER COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("ADD VALUE (", "DROP VALUE (", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2894,6 +2926,26 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("true", "false");
 	}
 
+	/* CREATE/ALTER/DROP COLUMN ... KEY */
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN"))
+		COMPLETE_WITH("ENCRYPTION", "MASTER");
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN", "ENCRYPTION|MASTER"))
+		COMPLETE_WITH("KEY");
+
+	/* CREATE COLUMN ENCRYPTION KEY */
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("VALUES");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH", "VALUES"))
+		COMPLETE_WITH("(");
+
+	/* CREATE COLUMN MASTER KEY */
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("(");
+
 	/* CREATE DATABASE */
 	else if (Matches("CREATE", "DATABASE", MatchAny))
 		COMPLETE_WITH("OWNER", "TEMPLATE", "ENCODING", "TABLESPACE",
@@ -3605,7 +3657,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 	else if (Matches("DISCARD"))
-		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP");
+		COMPLETE_WITH("ALL", "COLUMN ENCRYPTION KEYS", "PLANS", "SEQUENCES", "TEMP");
 
 /* DO */
 	else if (Matches("DO"))
@@ -3619,6 +3671,7 @@ psql_completion(const char *text, int start, int end)
 			 Matches("DROP", "ACCESS", "METHOD", MatchAny) ||
 			 (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
 			  ends_with(prev_wd, ')')) ||
+			 Matches("DROP", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
 			 Matches("DROP", "EVENT", "TRIGGER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "TABLE", MatchAny) ||
@@ -3931,6 +3984,8 @@ psql_completion(const char *text, int start, int end)
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"COLUMN ENCRYPTION KEY",
+											"COLUMN MASTER KEY",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4046,6 +4101,16 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+	/* Complete "GRANT/REVOKE * ON COLUMN ENCRYPTION|MASTER KEY *" with TO/FROM */
+	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
+			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny))
+	{
+		if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny, MatchAny))
+			COMPLETE_WITH("TO");
+		else
+			COMPLETE_WITH("FROM");
+	}
+
 	/* Complete "GRANT/REVOKE * ON FOREIGN DATA WRAPPER *" with TO/FROM */
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny))
diff --git a/src/common/Makefile b/src/common/Makefile
index 113029bf7b..73dce1150e 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			return "AEAD_AES_128_CBC_HMAC_SHA_256";
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			return "AEAD_AES_192_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			return "AEAD_AES_256_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			return "AEAD_AES_256_CBC_HMAC_SHA_512";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index 41bd58ebdf..3695d3285b 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -4,6 +4,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 747ecb800d..4e384bbcdb 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,9 +20,13 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
+extern void DiscardColumnEncryptionKeys(void);
+
 extern void debugStartup(DestReceiver *self, int operation,
 						 TupleDesc typeinfo);
 extern bool debugtup(TupleTableSlot *slot, DestReceiver *self);
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index ffd5e9dc82..eb59e73c0a 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -92,6 +92,9 @@ typedef enum ObjectClass
 	OCLASS_TYPE,				/* pg_type */
 	OCLASS_CAST,				/* pg_cast */
 	OCLASS_COLLATION,			/* pg_collation */
+	OCLASS_CEK,					/* pg_colenckey */
+	OCLASS_CEKDATA,				/* pg_colenckeydata */
+	OCLASS_CMK,					/* pg_colmasterkey */
 	OCLASS_CONSTRAINT,			/* pg_constraint */
 	OCLASS_CONVERSION,			/* pg_conversion */
 	OCLASS_DEFAULT,				/* pg_attrdef */
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index d01ab504b6..758696b539 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 3179be09d3..9e2c5256f7 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,9 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_colmasterkey.h',
+  'pg_colenckey.h',
+  'pg_colenckeydata.h',
 ]
 
 bki_data = [
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index f64a0ec26b..d0b9e8458d 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -115,6 +115,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c4d6adcd3e..c58b79e3a7 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5b950129de..0e9e85ebf3 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index b561e17781..7910175a6a 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,17 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	bool		attislocal BKI_DEFAULT(t);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey);
+
+	/*
+	 * User-visible type and typmod, currently used for encrypted columns.
+	 * These are only set to nondefault values if they are different from
+	 * atttypid and attypmod.
+	 */
+	Oid			attusertypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+	int32		attusertypmod BKI_DEFAULT(-1);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..c57fa18a27
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+/* ----------------
+ *		pg_colenckey definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckey
+ * ----------------
+ */
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+	Oid			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		cekacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_TOAST(pg_colenckey, 8263, 8264);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_nsp_index, 8242, ColumnEncKeyNameNspIndexId, on pg_colenckey using btree(cekname name_ops, ceknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkeydata.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+/* ----------------
+ *		pg_colenckeydata definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckeydata
+ * ----------------
+ */
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..d3bfd36279
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+	aclitem		cmkacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, on pg_colmasterkey using btree(cmkname name_ops, cmknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c867d99563..ff06d52fd0 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index b2cdea66c4..114279fa64 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 91587b99d0..c21052a3f7 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 505595620e..b410388a42 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6355,6 +6355,14 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -7142,6 +7150,68 @@
   proname => 'fmgr_sql_validator', provolatile => 's', prorettype => 'void',
   proargtypes => 'oid', prosrc => 'fmgr_sql_validator' },
 
+{ oid => '8265',
+  descr => 'user privilege on column encryption key by username, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_encryption_key_privilege_name_name' },
+{ oid => '8266',
+  descr => 'user privilege on column encryption key by username, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_encryption_key_privilege_name_id' },
+{ oid => '8267',
+  descr => 'user privilege on column encryption key by user oid, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_encryption_key_privilege_id_name' },
+{ oid => '8268',
+  descr => 'user privilege on column encryption key by user oid, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_encryption_key_privilege_id_id' },
+{ oid => '8269',
+  descr => 'current user privilege on column encryption key by column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_encryption_key_privilege_name' },
+{ oid => '8270',
+  descr => 'current user privilege on column encryption key by column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_encryption_key_privilege_id' },
+
+{ oid => '8271',
+  descr => 'user privilege on column master key by username, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_master_key_privilege_name_name' },
+{ oid => '8272',
+  descr => 'user privilege on column master key by username, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_master_key_privilege_name_id' },
+{ oid => '8273',
+  descr => 'user privilege on column master key by user oid, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_master_key_privilege_id_name' },
+{ oid => '8274',
+  descr => 'user privilege on column master key by user oid, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_master_key_privilege_id_id' },
+{ oid => '8275',
+  descr => 'current user privilege on column master key by column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_master_key_privilege_name' },
+{ oid => '8276',
+  descr => 'current user privilege on column master key by column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_master_key_privilege_id' },
+
 { oid => '2250',
   descr => 'user privilege on database by username, database name',
   proname => 'has_database_privilege', provolatile => 's', prorettype => 'bool',
@@ -11939,4 +12009,37 @@
   proname => 'any_value_transfn', prorettype => 'anyelement',
   proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8245',
+  proname => 'pg_encrypted_det_eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'pg_encrypted_det_ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 92bcaf2c73..b9d3920a97 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,17 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+# Note: typstorage 'e' since compression is not useful for encrypted data
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 519e570c8c..3c7ab2a8fe 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -294,6 +294,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPCATEGORY_USER		'U'
 #define  TYPCATEGORY_BITSTRING	'V' /* er ... "varbit"? */
 #define  TYPCATEGORY_UNKNOWN	'X'
+#define  TYPCATEGORY_ENCRYPTED	'Y'
 #define  TYPCATEGORY_INTERNAL	'Z'
 
 #define  TYPALIGN_CHAR			'c' /* char alignment (i.e. unaligned) */
diff --git a/src/include/commands/colenccmds.h b/src/include/commands/colenccmds.h
new file mode 100644
index 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/colenccmds.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COLENCCMDS_H
+#define COLENCCMDS_H
+
+#include "catalog/objectaddress.h"
+#include "parser/parse_node.h"
+
+extern ObjectAddress CreateCEK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt);
+extern ObjectAddress CreateCMK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e7c2b91a58..11f88c59ce 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,4 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml;.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index ac6407e9f6..39a286e58a 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -164,6 +164,7 @@ typedef struct Port
 	 */
 	char	   *database_name;
 	char	   *user_name;
+	bool		column_encryption_enabled;
 	char	   *cmdline_options;
 	List	   *guc_options;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f7d7f10f7d..1699e2a63f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -88,8 +88,7 @@ typedef uint64 AclMode;			/* a bitmask of privilege bits */
 #define ACL_REFERENCES	(1<<5)
 #define ACL_TRIGGER		(1<<6)
 #define ACL_EXECUTE		(1<<7)	/* for functions */
-#define ACL_USAGE		(1<<8)	/* for languages, namespaces, FDWs, and
-								 * servers */
+#define ACL_USAGE		(1<<8)	/* for various object types */
 #define ACL_CREATE		(1<<9)	/* for namespaces and databases */
 #define ACL_CREATE_TEMP (1<<10) /* for databases */
 #define ACL_CONNECT		(1<<11) /* for databases */
@@ -722,6 +721,7 @@ typedef struct ColumnDef
 	char	   *colname;		/* name of column */
 	TypeName   *typeName;		/* type of column */
 	char	   *compression;	/* compression method for column */
+	List	   *encryption;		/* encryption info for column */
 	int			inhcount;		/* number of times column is inherited */
 	bool		is_local;		/* column has local (non-inherited) def'n */
 	bool		is_not_null;	/* NOT NULL constraint specified? */
@@ -758,11 +758,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1980,6 +1981,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2167,6 +2171,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
@@ -3641,6 +3670,7 @@ typedef struct CheckPointStmt
 typedef enum DiscardMode
 {
 	DISCARD_ALL,
+	DISCARD_COLUMN_ENCRYPTION_KEYS,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
 	DISCARD_TEMP
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..c6da932042 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -149,6 +149,7 @@ PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("encryption", ENCRYPTION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -228,6 +229,7 @@ PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL)
 PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("language", LANGUAGE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("large", LARGE_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -250,6 +252,7 @@ PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("master", MASTER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index d4865e50f6..26e1cd617a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..a5e61c9f5b 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -29,6 +29,8 @@ PG_CMDTAG(CMDTAG_ALTER_ACCESS_METHOD, "ALTER ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_AGGREGATE, "ALTER AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CAST, "ALTER CAST", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_COLLATION, "ALTER COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY, "ALTER COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_MASTER_KEY, "ALTER COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONSTRAINT, "ALTER CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONVERSION, "ALTER CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_DATABASE, "ALTER DATABASE", false, false, false)
@@ -86,6 +88,8 @@ PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, fals
 PG_CMDTAG(CMDTAG_CREATE_AGGREGATE, "CREATE AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CAST, "CREATE CAST", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_COLLATION, "CREATE COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY, "CREATE COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_MASTER_KEY, "CREATE COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONSTRAINT, "CREATE CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONVERSION, "CREATE CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_DATABASE, "CREATE DATABASE", false, false, false)
@@ -130,6 +134,7 @@ PG_CMDTAG(CMDTAG_DECLARE_CURSOR, "DECLARE CURSOR", false, false, false)
 PG_CMDTAG(CMDTAG_DELETE, "DELETE", false, false, true)
 PG_CMDTAG(CMDTAG_DISCARD, "DISCARD", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
+PG_CMDTAG(CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS, "DISCARD COLUMN ENCRYPTION KEYS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false)
@@ -138,6 +143,8 @@ PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CAST, "DROP CAST", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_COLLATION, "DROP COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_ENCRYPTION_KEY, "DROP COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_MASTER_KEY, "DROP COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONSTRAINT, "DROP CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONVERSION, "DROP CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_DATABASE, "DROP DATABASE", false, false, false)
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2..0c73022833 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -159,6 +159,8 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
+#define ACL_ALL_RIGHTS_CEK			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_CMK			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..1dac24e4b4 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -163,6 +163,7 @@ extern bool type_is_rowtype(Oid typid);
 extern bool type_is_enum(Oid typid);
 extern bool type_is_range(Oid typid);
 extern bool type_is_multirange(Oid typid);
+extern bool type_is_encrypted(Oid typid);
 extern void get_type_category_preferred(Oid typid,
 										char *typcategory,
 										bool *typispreferred);
@@ -202,6 +203,9 @@ extern Oid	get_publication_oid(const char *pubname, bool missing_ok);
 extern char *get_publication_name(Oid pubid, bool missing_ok);
 extern Oid	get_subscription_oid(const char *subname, bool missing_ok);
 extern char *get_subscription_name(Oid subid, bool missing_ok);
+extern char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok);
+extern char *get_cmk_name(Oid cmkid, bool missing_ok);
 
 #define type_is_array(typid)  (get_element_type(typid) != InvalidOid)
 /* type_is_array_domain accepts both plain arrays and domains over arrays */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index a443181d41..8ecacb29df 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d5d50ceab4..bf1d527c1a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAMENSP,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAMENSP,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c18e914228..10dfda8016 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,7 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 8f80c35c89..df701f2df3 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -341,6 +341,14 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	{"cmklookup", "PGCMKLOOKUP", "", NULL,
+		"CMK-Lookup", "", 64,
+	offsetof(struct pg_conn, cmklookup)},
+
+	{"column_encryption", "PGCOLUMNENCRYPTION", "0", NULL,
+		"Column-Encryption", "", 1,
+	offsetof(struct pg_conn, column_encryption_setting)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -1412,6 +1420,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4056,6 +4086,22 @@ freePGconn(PGconn *conn)
 	free(conn->krbsrvname);
 	free(conn->gsslib);
 	free(conn->connip);
+	free(conn->cmklookup);
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		free(conn->cmks[i].cmkname);
+		free(conn->cmks[i].cmkrealm);
+	}
+	free(conn->cmks);
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		if (conn->ceks[i].cekdata)
+		{
+			explicit_bzero(conn->ceks[i].cekdata, conn->ceks[i].cekdatalen);
+			free(conn->ceks[i].cekdata);
+		}
+	}
+	free(conn->ceks);
 	/* Note that conn->Pfdebug is not ours to close or free */
 	free(conn->write_err_msg);
 	free(conn->inBuffer);
diff --git a/src/interfaces/libpq/fe-encrypt-openssl.c b/src/interfaces/libpq/fe-encrypt-openssl.c
new file mode 100644
index 0000000000..9baa47da13
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,839 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt-openssl.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe-encrypt.h"
+#include "libpq-int.h"
+
+#include "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+#define pqResultAlloc(res, nBytes, isBinary) malloc(nBytes)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = NULL;
+	EVP_PKEY_CTX *ctx = NULL;
+	unsigned char *out = NULL;
+	size_t		outlen;
+
+	switch (cmkalg)
+	{
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			md = EVP_sha1();
+			break;
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		free(out);
+		out = NULL;
+		goto fail;
+	}
+
+	*tolen = outlen;
+
+fail:
+	EVP_PKEY_CTX_free(ctx);
+	EVP_PKEY_free(key);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05;
+ * for encrypting and decrypting column values.
+ */
+
+#ifdef TEST_ENCRYPT
+
+/*
+ * Test data from
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5;
+ */
+
+/*
+ * The different test cases just use different prefixes of K, so one constant
+ * is enough here.
+ */
+static const unsigned char K[] = {
+	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+	0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+	0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+	0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+};
+
+static const unsigned char P[] = {
+	0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20,
+	0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75,
+	0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65,
+	0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62,
+	0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69,
+	0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66,
+	0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f,
+	0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65,
+};
+
+static const unsigned char test_IV[] = {
+	0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04,
+};
+
+static const unsigned char test_A[] = {
+	0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63,
+	0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20,
+	0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+static const EVP_CIPHER *
+pg_cekalg_to_openssl_cipher(int cekalg)
+{
+	const EVP_CIPHER *cipher;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			cipher = EVP_aes_128_cbc();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			cipher = EVP_aes_192_cbc();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			cipher = EVP_aes_256_cbc();
+			break;
+		default:
+			cipher = NULL;
+	}
+
+	return cipher;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+static const EVP_MD *
+pg_cekalg_to_openssl_md(int cekalg)
+{
+	const EVP_MD *md;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			md = EVP_sha384();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			md = EVP_sha512();
+			break;
+		default:
+			md = NULL;
+	}
+
+	return md;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+static int
+md_key_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 16;
+	else if (md == EVP_sha384())
+		return 24;
+	else if (md == EVP_sha512())
+		return 32;
+	else
+		return -1;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+static int
+md_hash_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 32;
+	else if (md == EVP_sha384())
+		return 48;
+	else if (md == EVP_sha512())
+		return 64;
+	else
+		return -1;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+static bool
+get_message_auth_tag(const EVP_MD *md,
+					 const unsigned char *mac_key, int mac_key_len,
+					 const unsigned char *encr, int encrlen,
+					 unsigned char *md_value, size_t *md_len_p,
+					 const char **errmsgp)
+{
+	static char msgbuf[1024];
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	int64		al;
+	bool		result = false;
+
+	if (encrlen < 0)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("encrypted value has invalid length"));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, mac_key, mac_key_len);
+	if (!pkey)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("could not allocate key for HMAC: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, buf, bufsize))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, md_len_p))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	Assert(*md_len_p == md_hash_length(md));
+
+	/* truncate output to half the length, per spec */
+	*md_len_p /= 2;
+
+	result = true;
+fail:
+	free(buf);
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+
+/*
+ * Decrypt a column value
+ */
+unsigned char *
+decrypt_value(PGresult *res, const PGCEK *cek, int cekalg, const unsigned char *input, int inputlen, const char **errmsgp)
+{
+	static char msgbuf[1024];
+
+	const unsigned char *iv = NULL;
+	size_t		ivlen;
+
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *decr;
+	int			decrlen,
+				decrlen2;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized encryption algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized digest algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("column encryption key has wrong length for algorithm (has: %zu, required: %d)"),
+				 cek->cekdatalen, key_len);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_memcmp(input + (inputlen - md_len), md_value, md_len) != 0)
+	{
+		*errmsgp = libpq_gettext("MAC mismatch");
+		goto fail;
+	}
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	iv = input;
+	input += ivlen;
+	inputlen -= ivlen;
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = inputlen + EVP_CIPHER_CTX_block_size(evp_cipher_ctx) + 1;
+	buf = pqResultAlloc(res, bufsize, false);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	decr = buf;
+	if (!EVP_DecryptUpdate(evp_cipher_ctx, decr, &decrlen, input, inputlen - md_len))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	if (!EVP_DecryptFinal_ex(evp_cipher_ctx, decr + decrlen, &decrlen2))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	decrlen += decrlen2;
+	Assert(decrlen < bufsize);
+	decr[decrlen] = '\0';
+	result = decr;
+
+fail:
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	return result;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#ifndef TEST_ENCRYPT
+static bool
+make_siv(PGconn *conn,
+		 unsigned char *iv, size_t ivlen,
+		 const EVP_MD *md,
+		 const unsigned char *iv_key, int iv_key_len,
+		 const unsigned char *plaintext, int plaintext_len)
+{
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	bool		result = false;
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, iv_key, iv_key_len);
+	if (!pkey)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	Assert(md_len == md_hash_length(md));
+	memcpy(iv, md_value, ivlen);
+
+	result = true;
+fail:
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+#endif							/* TEST_ENCRYPT */
+
+/*
+ * Encrypt a column value
+ */
+unsigned char *
+encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg, const unsigned char *value, int *nbytesp, bool enc_det)
+{
+	int			nbytes = *nbytesp;
+	unsigned char iv[EVP_MAX_IV_LENGTH];
+	size_t		ivlen;
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_key;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *encr;
+	int			encrlen,
+				encrlen2;
+
+	const char *errmsg;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		buf2size;
+	unsigned char *buf2 = NULL;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	Assert(ivlen <= sizeof(iv));
+	if (enc_det)
+	{
+#ifndef TEST_ENCRYPT
+		make_siv(conn, iv, ivlen, md, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	bufsize = ivlen + (nbytes + 2 * EVP_CIPHER_CTX_block_size(evp_cipher_ctx) - 1);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	encrlen += encrlen2;
+
+	encr -= ivlen;
+	encrlen += ivlen;
+
+	Assert(encrlen <= bufsize);
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  encr, encrlen,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf2, encr, encrlen);
+	memcpy(buf2 + encrlen, md_value, md_len);
+
+	result = buf2;
+	nbytes = buf2size;
+
+fail:
+	free(buf);
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	*nbytesp = nbytes;
+	return result;
+}
+
+
+/*
+ * Run test cases
+ */
+#ifdef TEST_ENCRYPT
+
+static void
+debug_print_hex(const char *name, const unsigned char *val, int len)
+{
+	printf("%s =", name);
+	for (int i = 0; i < len; i++)
+	{
+		if (i % 16 == 0)
+			printf("\n");
+		else
+			printf(" ");
+		printf("%02x", val[i]);
+	}
+	printf("\n");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, K_len);
+
+	C = encrypt_value(NULL, &cek, alg, P, &nbytes, true);
+	debug_print_hex("C", C, nbytes);
+}
+
+int
+main(int argc, char **argv)
+{
+	printf("5.1\n");
+	test_case(PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256, K, 32, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FE_ENCRYPT_H
+#define FE_ENCRYPT_H
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+extern unsigned char *decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+											int fromlen, const unsigned char *from,
+											int *tolen);
+
+extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg,
+									const unsigned char *input, int inputlen,
+									const char **errmsgp);
+
+extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg,
+									const unsigned char *value, int *nbytesp, bool enc_det);
+
+#endif							/* FE_ENCRYPT_H */
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index ec62550e38..128f8e3b96 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,420 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+int
+pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+					  const char *keyrealm)
+{
+	char	   *keyname_copy;
+	char	   *keyrealm_copy;
+	bool		found;
+
+	keyname_copy = strdup(keyname);
+	if (!keyname_copy)
+		return EOF;
+	keyrealm_copy = strdup(keyrealm);
+	if (!keyrealm_copy)
+	{
+		free(keyname_copy);
+		return EOF;
+	}
+
+	found = false;
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		struct pg_cmk *checkcmk = &conn->cmks[i];
+
+		/* replace existing? */
+		if (checkcmk->cmkid == keyid)
+		{
+			free(checkcmk->cmkname);
+			free(checkcmk->cmkrealm);
+			checkcmk->cmkname = keyname_copy;
+			checkcmk->cmkrealm = keyrealm_copy;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newncmks;
+		struct pg_cmk *newcmks;
+		struct pg_cmk *newcmk;
+
+		newncmks = conn->ncmks + 1;
+		if (newncmks <= 0)
+			return EOF;
+		newcmks = realloc(conn->cmks, newncmks * sizeof(struct pg_cmk));
+		if (!newcmks)
+		{
+			free(keyname_copy);
+			free(keyrealm_copy);
+			return EOF;
+		}
+
+		newcmk = &newcmks[newncmks - 1];
+		newcmk->cmkid = keyid;
+		newcmk->cmkname = keyname_copy;
+		newcmk->cmkrealm = keyrealm_copy;
+
+		conn->ncmks = newncmks;
+		conn->cmks = newcmks;
+	}
+
+	return 0;
+}
+
+/*
+ * Replace placeholders in input string.  Return value malloc'ed.
+ */
+static char *
+replace_cmk_placeholders(const char *in, const char *cmkname, const char *cmkrealm, int cmkalg, const char *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					p++;
+					break;
+				case 'r':
+					appendPQExpBufferStr(&buf, cmkrealm);
+					p++;
+					break;
+				default:
+					appendPQExpBufferChar(&buf, p[0]);
+			}
+		}
+		else
+			appendPQExpBufferChar(&buf, p[0]);
+	}
+
+	return buf.data;
+}
+
+#ifndef USE_SSL
+/*
+ * Dummy implementation for non-SSL builds
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	return NULL;
+}
+#endif
+
+/*
+ * Decrypt a CEK using the given CMK.  The ciphertext is passed in
+ * "from" and "fromlen".  Return the decrypted value in a malloc'ed area, its
+ * length via "tolen".  Return NULL on error; add error messages directly to
+ * "conn".
+ */
+static unsigned char *
+decrypt_cek(PGconn *conn, const PGCMK *cmk, int cmkalg,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	char	   *cmklookup;
+	bool		found = false;
+	unsigned char *result = NULL;
+
+	if (!conn->cmklookup || !conn->cmklookup[0])
+	{
+		libpq_append_conn_error(conn, "column master key lookup is not configured");
+		return NULL;
+	}
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		return NULL;
+	}
+
+	/*
+	 * Analyze semicolon-separated list
+	 */
+	for (char *s = strtok(cmklookup, ";"); s; s = strtok(NULL, ";"))
+	{
+		char	   *sep;
+
+		/* split found token at '=' */
+		sep = strchr(s, '=');
+		if (!sep)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', s);
+			break;
+		}
+
+		/* matching realm? */
+		if (strncmp(s, "*", sep - s) == 0 || strncmp(s, cmk->cmkrealm, sep - s) == 0)
+		{
+			char	   *sep2;
+
+			found = true;
+
+			sep2 = strchr(sep, ':');
+			if (!sep2)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, "r");
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+int
+pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, const unsigned char *value, int len)
+{
+	PGCMK	   *cmk = NULL;
+	unsigned char *plainval = NULL;
+	int			plainvallen = 0;
+	bool		found;
+
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		if (conn->cmks[i].cmkid == cmkid)
+		{
+			cmk = &conn->cmks[i];
+			break;
+		}
+	}
+
+	if (!cmk)
+		return EOF;
+
+	plainval = decrypt_cek(conn, cmk, cmkalg, len, value, &plainvallen);
+	if (!plainval)
+		return EOF;
+
+	found = false;
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		struct pg_cek *checkcek = &conn->ceks[i];
+
+		/* replace existing? */
+		if (checkcek->cekid == keyid)
+		{
+			free(checkcek->cekdata);
+			checkcek->cekdata = plainval;
+			checkcek->cekdatalen = plainvallen;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newnceks;
+		struct pg_cek *newceks;
+		struct pg_cek *newcek;
+
+		newnceks = conn->nceks + 1;
+		if (newnceks <= 0)
+		{
+			free(plainval);
+			return EOF;
+		}
+		newceks = realloc(conn->ceks, newnceks * sizeof(struct pg_cek));
+		if (!newceks)
+		{
+			free(plainval);
+			return EOF;
+		}
+
+		newcek = &newceks[newnceks - 1];
+		newcek->cekid = keyid;
+		newcek->cekdata = plainval;
+		newcek->cekdatalen = plainvallen;
+
+		conn->nceks = newnceks;
+		conn->ceks = newceks;
+	}
+
+	return 0;
+}
 
 /*
  * pqRowProcessor
@@ -1251,13 +1668,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == res->attDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					*errmsgp = libpq_gettext("protocol error: column encryption key associated with encrypted column was not sent by the server");
+					goto fail;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				if (val == NULL)
+					goto fail;
+#else
+				*errmsgp = libpq_gettext("column encryption not supported by this build");
 				goto fail;
+#endif
+			}
+			else
+			{
+				val = (char *) pqResultAlloc(res, clen + 1, isbinary);
+				if (val == NULL)
+					goto fail;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* copy and zero-terminate the data (even if it's binary) */
+				memcpy(val, columns[i].value, clen);
+				val[clen] = '\0';
+			}
 
 			tup[i].len = clen;
 			tup[i].value = val;
@@ -1500,6 +1955,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1973,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2012,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2128,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2171,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2270,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2319,47 @@ PQsendQueryGuts(PGconn *conn,
 		goto sendFailed;
 
 	/* Send parameter formats */
-	if (nParams > 0 && paramFormats)
+	if (nParams > 0 && (paramFormats || (paramDesc && paramDesc->paramDescs)))
 	{
 		if (pqPutInt(nParams, 2, conn) < 0)
 			goto sendFailed;
+
 		for (i = 0; i < nParams; i++)
 		{
-			if (pqPutInt(paramFormats[i], 2, conn) < 0)
+			int			format = paramFormats ? paramFormats[i] : 0;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2378,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2396,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				bool		enc_det = (paramDesc->paramDescs[i].flags & 0x01) != 0;
+				PGCEK	   *cek = NULL;
+				char	   *enc_paramValue;
+				int			enc_nbytes = nbytes;
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == paramDesc->paramDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					libpq_append_conn_error(conn, "protocol error: column encryption key associated with encrypted parameter was not sent by the server");
+					goto sendFailed;
+				}
+
+				enc_paramValue = (char *) encrypt_value(conn, cek, paramDesc->paramDescs[i].cekalg,
+														(const unsigned char *) paramValue, &enc_nbytes, enc_det);
+				if (!enc_paramValue)
+					goto sendFailed;
+
+				if (pqPutInt(enc_nbytes, 4, conn) < 0 ||
+					pqPutnchar(enc_paramValue, enc_nbytes, conn) < 0)
+					goto sendFailed;
+
+				free(enc_paramValue);
+#else
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2878,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4146,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4194,17 @@ PQfmod(const PGresult *res, int field_num)
 		return 0;
 }
 
+int
+PQfisencrypted(const PGresult *res, int field_num)
+{
+	if (!check_field_number(res, field_num))
+		return false;
+	if (res->attDescs)
+		return (res->attDescs[field_num].cekid != 0);
+	else
+		return false;
+}
+
 char *
 PQcmdStatus(PGresult *res)
 {
@@ -3762,6 +4390,17 @@ PQparamtype(const PGresult *res, int param_num)
 		return InvalidOid;
 }
 
+int
+PQparamisencrypted(const PGresult *res, int param_num)
+{
+	if (!check_param_number(res, param_num))
+		return false;
+	if (res->paramDescs)
+		return (res->paramDescs[param_num].cekid != 0);
+	else
+		return false;
+}
+
 
 /* PQsetnonblocking:
  *	sets the PGconn's database connection non-blocking if the arg is true
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 8ab6a88416..617e641cf6 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -43,6 +43,8 @@ static int	getRowDescriptions(PGconn *conn, int msgLength);
 static int	getParamDescriptions(PGconn *conn, int msgLength);
 static int	getAnotherTuple(PGconn *conn, int msgLength);
 static int	getParameterStatus(PGconn *conn);
+static int	getColumnMasterKey(PGconn *conn);
+static int	getColumnEncryptionKey(PGconn *conn);
 static int	getNotify(PGconn *conn);
 static int	getCopyStart(PGconn *conn, ExecStatusType copytype);
 static int	getReadyForQuery(PGconn *conn);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -358,8 +366,21 @@ pqParseInput3(PGconn *conn)
 					}
 					break;
 				case 't':		/* Parameter Description */
-					if (getParamDescriptions(conn, msgLength))
-						return;
+					if (conn->error_result ||
+						(conn->result != NULL &&
+						 conn->result->resultStatus == PGRES_FATAL_ERROR))
+					{
+						/*
+						 * We've already choked for some reason.  Just discard
+						 * the data till we get to the end of the query.
+						 */
+						conn->inCursor += msgLength;
+					}
+					else
+					{
+						if (getParamDescriptions(conn, msgLength))
+							return;
+					}
 					break;
 				case 'D':		/* Data Row */
 					if (conn->result != NULL &&
@@ -547,6 +568,9 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		int			typlen;
 		int			atttypmod;
 		int			format;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGets(&conn->workBuffer, conn) ||
 			pqGetInt(&tableid, 4, conn) ||
@@ -561,6 +585,23 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, conn) ||
+				pqGetInt(&flags, 2, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -582,6 +623,8 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		result->attDescs[i].typid = typid;
 		result->attDescs[i].typlen = typlen;
 		result->attDescs[i].atttypmod = atttypmod;
+		result->attDescs[i].cekid = cekid;
+		result->attDescs[i].cekalg = cekalg;
 
 		if (format != 1)
 			result->binary = 0;
@@ -685,10 +728,31 @@ getParamDescriptions(PGconn *conn, int msgLength)
 	for (i = 0; i < nparams; i++)
 	{
 		int			typid;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGetInt(&typid, 4, conn))
 			goto not_enough_data;
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&cekalg, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&flags, 2, conn))
+				goto not_enough_data;
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
 		result->paramDescs[i].typid = typid;
+		result->paramDescs[i].cekid = cekid;
+		result->paramDescs[i].cekalg = cekalg;
+		result->paramDescs[i].flags = flags;
 	}
 
 	/* Success! */
@@ -1468,6 +1532,92 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+/*
+ * Attempt to read a ColumnMasterKey message.
+ * Entry: 'y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	int			keyid;
+	char	   *keyname;
+	char	   *keyrealm;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	if (!keyname)
+		return EOF;
+	/* Get the key realm */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyrealm = strdup(conn->workBuffer.data);
+	if (!keyrealm)
+		return EOF;
+	/* And save it */
+	ret = pqSaveColumnMasterKey(conn, keyid, keyname, keyrealm);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(keyname);
+	free(keyrealm);
+
+	return ret;
+}
+
+/*
+ * Attempt to read a ColumnEncryptionKey message.
+ * Entry: 'Y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	int			keyid;
+	int			cmkid;
+	int			cmkalg;
+	char	   *buf;
+	int			vallen;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK ID */
+	if (pqGetInt(&cmkid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK algorithm */
+	if (pqGetInt(&cmkalg, 4, conn) != 0)
+		return EOF;
+	/* Get the key data len */
+	if (pqGetInt(&vallen, 4, conn) != 0)
+		return EOF;
+	/* Get the key data */
+	buf = malloc(vallen);
+	if (!buf)
+		return EOF;
+	if (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2436,9 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	if (conn->column_encryption_enabled)
+		ADD_STARTUP_OPTION("_pq_.column_encryption", "1");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index abaab6a073..77791e8349 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -450,7 +450,8 @@ pqTraceOutputS(FILE *f, const char *message, int *cursor)
 
 /* ParameterDescription */
 static void
-pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -458,12 +459,21 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 	nfields = pqTraceOutputInt16(f, message, cursor);
 
 	for (int i = 0; i < nfields; i++)
+	{
 		pqTraceOutputInt32(f, message, cursor, regress);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+			pqTraceOutputInt16(f, message, cursor);
+		}
+	}
 }
 
 /* RowDescription */
 static void
-pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -479,6 +489,11 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
 		pqTraceOutputInt16(f, message, cursor);
 		pqTraceOutputInt32(f, message, cursor, false);
 		pqTraceOutputInt16(f, message, cursor);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+		}
 	}
 }
 
@@ -514,6 +529,30 @@ pqTraceOutputW(FILE *f, const char *message, int *cursor, int length)
 		pqTraceOutputInt16(f, message, cursor);
 }
 
+/* ColumnMasterKey */
+static void
+pqTraceOutputy(FILE *f, const char *message, int *cursor, bool regress)
+{
+	fprintf(f, "ColumnMasterKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+}
+
+/* ColumnEncryptionKey */
+static void
+pqTraceOutputY(FILE *f, const char *message, int *cursor, bool regress)
+{
+	int			len;
+
+	fprintf(f, "ColumnEncryptionKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, false);
+	len = pqTraceOutputInt32(f, message, cursor, false);
+	pqTraceOutputNchar(f, len, message, cursor);
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -647,10 +686,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 				fprintf(conn->Pfdebug, "Sync"); /* no message content */
 			break;
 		case 't':				/* Parameter Description */
-			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'T':				/* Row Description */
-			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'v':				/* Negotiate Protocol Version */
 			pqTraceOutputv(conn->Pfdebug, message, &logCursor);
@@ -665,6 +706,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			fprintf(conn->Pfdebug, "Terminate");
 			/* No message content */
 			break;
+		case 'y':
+			pqTraceOutputy(conn->Pfdebug, message, &logCursor, regress);
+			break;
+		case 'Y':
+			pqTraceOutputY(conn->Pfdebug, message, &logCursor, regress);
+			break;
 		case 'Z':				/* Ready For Query */
 			pqTraceOutputZ(conn->Pfdebug, message, &logCursor);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f3d9220496..cf339a3e53 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -267,6 +267,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -438,6 +440,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,7 @@ extern int	PQfformat(const PGresult *res, int field_num);
 extern Oid	PQftype(const PGresult *res, int field_num);
 extern int	PQfsize(const PGresult *res, int field_num);
 extern int	PQfmod(const PGresult *res, int field_num);
+extern int	PQfisencrypted(const PGresult *res, int field_num);
 extern char *PQcmdStatus(PGresult *res);
 extern char *PQoidStatus(const PGresult *res);	/* old and ugly */
 extern Oid	PQoidValue(const PGresult *res);	/* new and improved */
@@ -540,6 +559,7 @@ extern int	PQgetlength(const PGresult *res, int tup_num, int field_num);
 extern int	PQgetisnull(const PGresult *res, int tup_num, int field_num);
 extern int	PQnparams(const PGresult *res);
 extern Oid	PQparamtype(const PGresult *res, int param_num);
+extern int	PQparamisencrypted(const PGresult *res, int param_num);
 
 /* Describe prepared statements and portals */
 extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d7ec5ed429..cbfaa7f95f 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -112,6 +112,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -343,6 +346,26 @@ typedef struct pg_conn_host
 								 * found in password file. */
 } pg_conn_host;
 
+/*
+ * Column encryption support data
+ */
+
+/* column master key */
+typedef struct pg_cmk
+{
+	Oid			cmkid;
+	char	   *cmkname;
+	char	   *cmkrealm;
+} PGCMK;
+
+/* column encryption key */
+typedef struct pg_cek
+{
+	Oid			cekid;
+	unsigned char *cekdata;		/* (decrypted) */
+	size_t		cekdatalen;
+} PGCEK;
+
 /*
  * PGconn stores all the state data associated with a single connection
  * to a backend.
@@ -396,6 +419,8 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *cmklookup;		/* CMK lookup specification */
+	char	   *column_encryption_setting;	/* column_encryption connection parameter (0 or 1) */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -477,6 +502,13 @@ struct pg_conn
 	PGVerbosity verbosity;		/* error/notice message verbosity */
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
+	bool		column_encryption_enabled;	/* parsed version of column_encryption_setting */
+
+	/* Column encryption support data */
+	int			ncmks;
+	PGCMK	   *cmks;
+	int			nceks;
+	PGCEK	   *ceks;
 
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
@@ -673,6 +705,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern int	pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+								  const char *keyrealm);
+extern int	pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg,
+									  const unsigned char *value, int len);
 extern int	pqRowProcessor(PGconn *conn, const char **errmsgp);
 extern void pqCommandQueueAdvance(PGconn *conn);
 extern int	PQsendQueryContinue(PGconn *conn, const char *query);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 573fd9b6ea..a6e6b2ada9 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,7 @@ endif
 
 if ssl.found()
   libpq_sources += files('fe-secure-common.c')
+  libpq_sources += files('fe-encrypt-openssl.c')
   libpq_sources += files('fe-secure-openssl.c')
 endif
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': get_option('ssl')},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 4df544ecef..0c36aa5f32 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
+GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-encrypt-openssl.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
 GETTEXT_TRIGGERS = libpq_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext pqInternalNotice:2
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 100644
--- a/src/interfaces/libpq/test/Makefile
+++ b/src/interfaces/libpq/test/Makefile
@@ -15,10 +15,17 @@ override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
 LDFLAGS_INTERNAL += $(libpq_pgport)
 
 PROGS = libpq_testclient libpq_uri_regress
+ifeq ($(with_ssl),openssl)
+PROGS += libpq_test_encrypt
+endif
+
 
 all: $(PROGS)
 
 $(PROGS): $(WIN32RES)
 
+libpq_test_encrypt: ../fe-encrypt-openssl.c
+	$(CC) $(CPPFLAGS) -DTEST_ENCRYPT $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 clean distclean maintainer-clean:
 	rm -f $(PROGS) *.o
diff --git a/src/interfaces/libpq/test/meson.build b/src/interfaces/libpq/test/meson.build
index b2a4b06fd2..87d2808b52 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c8ba170503 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -24,7 +24,7 @@ ifeq ($(with_ldap),yes)
 SUBDIRS += ldap
 endif
 ifeq ($(with_ssl),openssl)
-SUBDIRS += ssl
+SUBDIRS += column_encryption ssl
 endif
 
 # Test suites that are not safe by default but can be run if selected
@@ -36,7 +36,7 @@ export PG_TEST_EXTRA
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl column_encryption)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..d5ead874e5
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/column_encryption/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+export OPENSSL PERL
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/meson.build b/src/test/column_encryption/meson.build
new file mode 100644
index 0000000000..47f88c41ce
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,24 @@
+column_encryption_test_client = executable('test_client',
+  files('test_client.c'),
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+testprep_targets += column_encryption_test_client
+
+tests += {
+  'name': 'column_encryption',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_column_encryption.pl',
+      't/002_cmk_rotation.pl',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+      'PERL': perl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..c56af737ac
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,255 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+my $perl = $ENV{PERL};
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$openssl, 'pkeyutl', '-encrypt',
+		'-inkey', $cmkfilename,
+		'-pkeyopt', 'rsa_padding_mode:oaep',
+		'-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+		'-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+	# create CEK in database
+	$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = ${cmkname}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c smallint ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# Expected ciphertext length is 2 blocks of AES output (2 * 16) plus
+# half SHA-256 output (16) in hex encoding: (2 * 16 + 16) * 2 = 96
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1) TO STDOUT}),
+	qr/1\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text
+c|smallint),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result');
+
+{
+	local $ENV{PGCMKLOOKUP} = '*=run:broken %k %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = "*=run:$perl ./test_run_decrypt.pl %k %a %p";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$node->command_fails_like(['test_client', 'test1'], qr/not encrypted/, 'test client fails because parameters not encrypted');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result after test client insert');
+
+$node->command_ok(['test_client', 'test2'], 'test client test 2');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3|33),
+	'decrypted query result after test client insert 2');
+
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1 WHERE a = 3) TO STDOUT}),
+	qr/3\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3upd|33),
+	'decrypted query result after update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+is($node->safe_psql('postgres', q{SELECT b, count(*) FROM tbl2 GROUP BY b ORDER BY 2}),
+	q(valB|1
+valA|2),
+	'group by deterministically encrypted column');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl3 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c text ENCRYPTED WITH (column_encryption_key = cek2, algorithm = 'AEAD_AES_192_CBC_HMAC_SHA_384')
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1
+2|valB2|valC2
+3|valB3|valC3),
+	'decrypted query result multiple keys after second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..14eafb8ec9
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Test column master key rotation.  First, we generate CMK1 and a CEK
+# encrypted with it.  Then we add a CMK2 and encrypt the CEK with it
+# as well.  (Recall that a CEK can be associated with multiple CMKs,
+# for this reason.  That's why pg_colenckeydata is split out from
+# pg_colenckey.)  Then we remove CMK1.  We test that we can get
+# decrypted query results at each step.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# generate random bytes
+system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+# encrypt CEK using CMK
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk1filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# create CEK in database
+$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = cmk1, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b) VALUES (1, $1) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with one CMK');
+
+
+# create new CMK
+my $cmk2filename = create_cmk('cmk2');
+
+# encrypt CEK using new CMK
+#
+# (Here, we still have the plaintext of the CEK available from
+# earlier.  In reality, one would decrypt the CEK with the first CMK
+# and then re-encrypt it with the second CMK.)
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk2filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+$cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# add new data record for CEK in database
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} ADD VALUE (column_master_key = cmk2, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with two CMKs');
+
+
+# delete CEK record for first CMK
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} DROP VALUE (column_master_key = cmk1);});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with only new CMK');
+
+
+done_testing();
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (!(!PQparamisencrypted(res2, 0) &&
+		  PQparamisencrypted(res2, 1)))
+	{
+		fprintf(stderr, "wrong results from PQparamisencrypted()\n");
+		return 1;
+	}
+
+	res = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	PGconn	   *conn;
+	int			ret = 0;
+
+	conn = PQconnectdb("");
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		fprintf(stderr, "Connection to database failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (argc < 2 || argv[1] == NULL)
+		return 87;
+	else if (strcmp(argv[1], "test1") == 0)
+		ret = test1(conn);
+	else if (strcmp(argv[1], "test2") == 0)
+		ret = test2(conn);
+	else if (strcmp(argv[1], "test3") == 0)
+		ret = test3(conn);
+	else if (strcmp(argv[1], "test4") == 0)
+		ret = test4(conn);
+	else
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+
+# Test/sample command for libpq cmklookup run scheme
+#
+# This just places the data into temporary files and runs the openssl
+# command on it.  (In practice, this could more simply be written as a
+# shell script, but this way it's more portable.)
+
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $fh, '<:raw', "${tmpdir}/output.tmp" or die $!;
+my $data = '';
+
+while (1) {
+	my $success = read $fh, $data, 100, length($data);
+	die $! if not defined $success;
+	last if not $success;
+}
+
+close $fh;
+
+unlink "${tmpdir}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..d55ddb1ab7 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,7 @@ subdir('subscription')
 subdir('modules')
 
 if ssl.found()
+  subdir('column_encryption')
   subdir('ssl')
 endif
 
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..df47e36280
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,451 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE ROLE regress_enc_user1;
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+CREATE COLUMN MASTER KEY cmk2;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "cek1" already has data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "fail" does not exist
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+ERROR:  column encryption key "notexist" does not exist
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+\d tbl_29f3
+              Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_29f3
+                                        Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+\d tbl_447f
+              Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_447f
+                                        Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  permission denied for column master key cmk1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+ERROR:  permission denied for column encryption key cek1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column master key cmk1 depends on column master key cmk1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ERROR:  column master key "cmk3" already exists in schema "public"
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+ERROR:  column master key "cmkx" does not exist
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ERROR:  column encryption key "cek3" already exists in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+ERROR:  must be owner of column encryption key cek3
+DROP COLUMN MASTER KEY cmk3;  -- fail
+ERROR:  must be owner of column master key cmk3
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ERROR:  column encryption key "cek1" has no data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ERROR:  column master key "fail" does not exist
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+ERROR:  attribute "algorithm" must not be specified
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+ERROR:  column encryption key "fail" does not exist
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf..77fedce8aa 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -107,7 +109,8 @@ BEGIN
         ('text search template'), ('text search configuration'),
         ('policy'), ('user mapping'), ('default acl'), ('transform'),
         ('operator of access method'), ('function of access method'),
-        ('publication namespace'), ('publication relation')
+        ('publication namespace'), ('publication relation'),
+        ('column encryption key'), ('column encryption key data'), ('column master key')
     LOOP
         FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
         LOOP
@@ -327,6 +330,24 @@ WARNING:  error for publication relation,{addr_nsp,zwei},{}: argument list lengt
 WARNING:  error for publication relation,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist
 WARNING:  error for publication relation,{eins,zwei,drei},{}: argument list length must be exactly 1
 WARNING:  error for publication relation,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei"
+WARNING:  error for column encryption key,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins},{}: column master key "eins" does not exist
+WARNING:  error for column master key,{eins},{integer}: column master key "eins" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 -- these object types cannot be qualified names
 SELECT pg_get_object_address('language', '{one}', '{}');
 ERROR:  language "one" does not exist
@@ -409,6 +430,9 @@ WITH objects (type, name, args) AS (VALUES
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,8 @@ drop cascades to user mapping for regress_addr_user on server integer
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 DROP SCHEMA addr_nsp CASCADE;
 NOTICE:  drop cascades to 14 other objects
 DETAIL:  drop cascades to text search dictionary addr_ts_dict
@@ -547,6 +576,9 @@ WITH objects (classid, objid, objsubid) AS (VALUES
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|NULL
+("(""column master key"",,,)")|("(""column master key"",,)")|NULL
+("(""column encryption key"",,,)")|("(""column encryption key"",,)")|NULL
+("(""column encryption key data"",,,)")|("(""column encryption key data"",,)")|NULL
 -- restore normal output mode
 \a\t
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..226f5e404e 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attusertypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,9 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,13 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
  anyrange                    | anymultirange
-(5 rows)
+(6 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -872,6 +876,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+pg_encrypted_det_eq(pg_encrypted_det,pg_encrypted_det)
+pg_encrypted_det_ne(pg_encrypted_det,pg_encrypted_det)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -75,7 +75,9 @@ ORDER BY t1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | pg_encrypted_det
+ 8244 | pg_encrypted_rnd
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT t1.oid, t1.typname as basetype, t2.typname as arraytype,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 15e015b3d6..577a6d4b2e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 427429975e..57c7d2bec3 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..8d6320015c
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,297 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE ROLE regress_enc_user1;
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk2;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+\d tbl_29f3
+\d+ tbl_29f3
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+
+\d tbl_447f
+\d+ tbl_447f
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+
+
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+
+
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..35af43032f 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -99,7 +101,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
         ('text search template'), ('text search configuration'),
         ('policy'), ('user mapping'), ('default acl'), ('transform'),
         ('operator of access method'), ('function of access method'),
-        ('publication namespace'), ('publication relation')
+        ('publication namespace'), ('publication relation'),
+        ('column encryption key'), ('column encryption key data'), ('column master key')
     LOOP
         FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
         LOOP
@@ -174,6 +177,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -228,6 +234,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 
 DROP SCHEMA addr_nsp CASCADE;
 
@@ -247,6 +255,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 71a75626d5271f2bcdbdc43b8c13065c4634fd9f
-- 
2.39.2



Attachments:

  [text/plain] v17-0001-Automatic-client-side-column-level-encryption.patch (464.1K, ../../[email protected]/2-v17-0001-Automatic-client-side-column-level-encryption.patch)
  download | inline diff:
From 65eef95ef3a89178378d36fcd65a67c0d1592693 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Tue, 28 Feb 2023 21:23:25 +0100
Subject: [PATCH v17] Automatic client-side column-level encryption

This feature enables the automatic encryption and decryption of
particular columns in the client.  The data for those columns then
only ever appears in ciphertext on the server, so it is protected from
DBAs, sysadmins, cloud operators, etc. as well as accidental leakage
to server logs, file-system backups, etc.  The canonical use case for
this feature is storing credit card numbers encrypted, in accordance
with PCI DSS, as well as similar situations involving social security
numbers etc.  One can't do any computations with encrypted values on
the server, but for these use cases, that is not necessary.  This
feature does support deterministic encryption as an alternative to the
default randomized encryption, so in that mode one can do equality
lookups, at the cost of some security.

This functionality also exists in other database products, and the
overall concepts were mostly adopted from there.

(Note: This feature has nothing to do with any on-disk encryption
feature.  Both can exist independently.)

You declare a column as encrypted in a CREATE TABLE statement.  The
column value is encrypted by a symmetric key called the column
encryption key (CEK).  The CEK is a catalog object.  The CEK key
material is in turn encrypted by an asymmetric key called the column
master key (CMK).  The CMK is not stored in the database but somewhere
where the client can get to it, for example in a file or in a key
management system.  When a server sends rows containing encrypted
column values to the client, it first sends the required CMK and CEK
information (new protocol messages), which the client needs to record.
Then, the client can use this information to automatically decrypt the
incoming row data and forward it in plaintext to the application.

For the CMKs, libpq has a new connection parameter "cmklookup" that
specifies via a mini-language where to get the keys.  Right now, you
can use "file" to read it from a file, or "run" to run some program,
which could get it from a KMS.

The general idea would be for an application to have one CMK per area
of secret stuff, for example, for credit card data.  The CMK can be
rotated: each CEK can be represented multiple times in the database,
encrypted by a different CMK.  (The CEK can't be rotated easily, since
that would require reading out all the data from a table/column and
reencrypting it.  We could/should add some custom tooling for that,
but it wouldn't be a routine operation.)

Several encryption algorithms are provided.  The CMK process uses
RSAES_OAEP_SHA_1 or _256.  The CEK process uses
AEAD_AES_*_CBC_HMAC_SHA_* with several strengths.

In the server, the encrypted datums are stored in types called
pg_encrypted_rnd and pg_encrypted_det (for randomized and
deterministic encryption).  These are essentially cousins of bytea.
For the rest of the database system below the protocol handling, there
is nothing special about those.  For example, pg_encrypted_rnd has no
operators at all, pg_encrypted_det has only an equality operator.
pg_attribute has a new column attrealtypid that stores the original
type of the data in the column.  This is only used for providing it to
clients, so that higher-level clients can convert the decrypted value
to their appropriate data types in their environments.

The protocol extensions are guarded by a new protocol extension option
"_pq_.column_encryption".  If this is not set, nothing changes, the
protocol stays the same, and no encryption or decryption happens.

To get automatically encrypted data into the database (as opposed to
reading it out), it is required to use protocol-level prepared
statements (i.e., extended query).  The client must first prepare a
statement, then describe the statement to get parameter metadata,
which indicates which parameters are to be encrypted and how.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this if the
\bind command is used.

Another challenge is that the parse analysis must check which
underlying column a parameter corresponds to.  This is similar to
resorigtbl and resorigcol in the opposite direction.

Discussion: https://www.postgresql.org/message-id/flat/89157929-c2b6-817b-6025-8e4b2d89d88f%40enterprisedb.com
---
 doc/src/sgml/acronyms.sgml                    |  18 +
 doc/src/sgml/catalogs.sgml                    | 317 +++++++
 doc/src/sgml/charset.sgml                     |  10 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 444 +++++++++
 doc/src/sgml/func.sgml                        |  60 ++
 doc/src/sgml/glossary.sgml                    |  26 +
 doc/src/sgml/libpq.sgml                       | 322 +++++++
 doc/src/sgml/protocol.sgml                    | 467 ++++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |  10 +
 .../ref/create_column_encryption_key.sgml     | 173 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  55 +-
 doc/src/sgml/ref/discard.sgml                 |  14 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/grant.sgml                   |  12 +-
 doc/src/sgml/ref/pg_dump.sgml                 |  42 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   8 +
 src/backend/access/common/printtup.c          | 237 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  60 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 288 ++++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  17 +
 src/backend/commands/colenccmds.c             | 439 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/discard.c                |   8 +-
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 204 ++++-
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 200 ++++-
 src/backend/parser/parse_param.c              | 157 ++++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  56 ++
 src/backend/utils/adt/acl.c                   | 398 +++++++++
 src/backend/utils/adt/varlena.c               | 107 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  42 +
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/dumputils.c                   |   4 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 377 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  35 +
 src/bin/pg_dump/pg_dump_sort.c                |  14 +
 src/bin/pg_dump/pg_dumpall.c                  |   5 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  72 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 191 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  69 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   4 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |  11 +
 src/include/catalog/pg_colenckey.h            |  46 +
 src/include/catalog/pg_colenckeydata.h        |  46 +
 src/include/catalog/pg_colmasterkey.h         |  47 +
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  10 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               | 103 +++
 src/include/catalog/pg_type.dat               |  13 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  44 +-
 src/include/parser/kwlist.h                   |   3 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   7 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 839 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 671 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 157 +++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  24 +
 .../t/001_column_encryption.pl                | 255 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 451 ++++++++++
 src/test/regress/expected/object_address.out  |  37 +-
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 297 +++++++
 src/test/regress/sql/object_address.sql       |  13 +-
 src/test/regress/sql/type_sanity.sql          |   2 +
 144 files changed, 10399 insertions(+), 91 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/alter_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_master_key.sgml
 create mode 100644 src/backend/commands/colenccmds.c
 create mode 100644 src/common/colenc.c
 create mode 100644 src/include/catalog/pg_colenckey.h
 create mode 100644 src/include/catalog/pg_colenckeydata.h
 create mode 100644 src/include/catalog/pg_colmasterkey.h
 create mode 100644 src/include/commands/colenccmds.h
 create mode 100644 src/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 create mode 100644 src/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/meson.build
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/t/002_cmk_rotation.pl
 create mode 100644 src/test/column_encryption/test_client.c
 create mode 100755 src/test/column_encryption/test_run_decrypt.pl
 create mode 100644 src/test/regress/expected/column_encryption.out
 create mode 100644 src/test/regress/sql/column_encryption.sql

diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 2df6559acc..3a5f2c254c 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -56,6 +56,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CEK</acronym></term>
+    <listitem>
+     <para>
+      Column Encryption Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CIDR</acronym></term>
     <listitem>
@@ -67,6 +76,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CMK</acronym></term>
+    <listitem>
+     <para>
+      Column Master Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CPAN</acronym></term>
     <listitem>
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1e4048054..808f29669d 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -105,6 +105,21 @@ <title>System Catalogs</title>
       <entry>collations (locale information)</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link></entry>
+      <entry>column encryption keys</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link></entry>
+      <entry>column encryption key data</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link></entry>
+      <entry>column master keys</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link></entry>
       <entry>check constraints, unique constraints, primary key constraints, foreign key constraints</entry>
@@ -1360,6 +1375,44 @@ <title><structname>pg_attribute</structname> Columns</title>
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attcek</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, a reference to the column encryption key, else 0.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type of the
+       encrypted data that is reported to the client.  If the column is not
+       encrypted, then 0.  For encrypted columns, the field
+       <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.
+       </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type
+       modifier (analogous to <structfield>atttypmod</structfield>) that is
+       reported to the client.  If the column is not encrypted, then -1.  For
+       encrypted columns, the field <structfield>atttypmod</structfield>)
+       contains the identifier of the encryption algorithm; see <xref
+       linkend="protocol-cek"/> for possible values.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>attinhcount</structfield> <type>int4</type>
@@ -2467,6 +2520,270 @@ <title><structname>pg_collation</structname> Columns</title>
   </para>
  </sect1>
 
+ <sect1 id="catalog-pg-colenckey">
+  <title><structname>pg_colenckey</structname></title>
+
+  <indexterm zone="catalog-pg-colenckey">
+   <primary>pg_colenckey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckey</structname> contains information
+   about the column encryption keys in the database.  The actual key material
+   of the column encryption keys is in the catalog <link
+   linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link>.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column encryption key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colenckeydata">
+  <title><structname>pg_colenckeydata</structname></title>
+
+  <indexterm zone="catalog-pg-colenckeydata">
+   <primary>pg_colenckeydata</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckeydata</structname> contains the key
+   material of column encryption keys.  Each column encryption key object can
+   contain several versions of the key material, each encrypted with a
+   different column master key.  That allows the gradual rotation of the
+   column master keys.  Thus, <literal>(ckdcekid, ckdcmkid)</literal> is a
+   unique key of this table.
+  </para>
+
+  <para>
+   The key material of column encryption keys should never be decrypted inside
+   the database instance.  It is meant to be sent as-is to the client, where
+   it is decrypted using the associated column master key, and then used to
+   encrypt or decrypt column values.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckeydata</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcekid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column encryption key this entry belongs to
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column master key that the key material is encrypted with
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkalg</structfield> <type>int4</type>
+      </para>
+      <para>
+       The encryption algorithm used for encrypting the key material; see
+       <xref linkend="protocol-cmk"/> for possible values.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdencval</structfield> <type>bytea</type>
+      </para>
+      <para>
+       The key material of this column encryption key, encrypted using the
+       referenced column master key
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colmasterkey">
+  <title><structname>pg_colmasterkey</structname></title>
+
+  <indexterm zone="catalog-pg-colmasterkey">
+   <primary>pg_colmasterkey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colmasterkey</structname> contains information
+   about column master keys.  The keys themselves are not stored in the
+   database.  The catalog entry only contains information that is used by
+   clients to locate the keys, for example in a file or in a key management
+   system.
+  </para>
+
+  <table>
+   <title><structname>pg_colmasterkey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column master key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkrealm</structfield> <type>text</type>
+      </para>
+      <para>
+       A <quote>realm</quote> associated with this column master key.  This is
+       a freely chosen string that is used by clients to determine how to look
+       up the key.  A typical configuration would put all CMKs that are looked
+       up in the same way into the same realm.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml
index 3032392b80..f3026fff83 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,16 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When automatic client-side column-level encryption is used, then no
+     encoding conversion is possible.  (The encoding conversion happens on the
+     server, and the server cannot look inside any encrypted column values.)
+     If automatic client-side column-level encryption is enabled for a
+     session, then the server enforces that the client encoding matches the
+     server encoding, and any attempts to change the client encoding will be
+     rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..67fce16872 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <title>Pseudo-Types</title>
 
   </sect1>
 
+  <sect1 id="datatype-encrypted">
+   <title>Types Related to Encryption</title>
+
+   <para>
+    An encrypted column value (see <xref linkend="ddl-column-encryption"/>) is
+    internally stored using the types
+    <type>pg_encrypted_rnd</type> (for randomized encryption) or
+    <type>pg_encrypted_det</type> (for deterministic encryption); see <xref
+    linkend="datatype-encrypted-table"/>.  Most of the database system treats
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support automatic client-side column-level encryption
+    or have disabled it will see the encrypted values in this format.  Clients
+    that support automatic client-side column-level encryption will not see
+    these types in result sets, as the protocol layer will translate them back
+    to the declared underlying type in the table definition.
+   </para>
+
+    <table id="datatype-encrypted-table">
+     <title>Types Related to Encryption</title>
+     <tgroup cols="3">
+      <colspec colname="col1" colwidth="1*"/>
+      <colspec colname="col2" colwidth="3*"/>
+      <colspec colname="col3" colwidth="2*"/>
+      <thead>
+       <row>
+        <entry>Name</entry>
+        <entry>Storage Size</entry>
+        <entry>Description</entry>
+       </row>
+      </thead>
+      <tbody>
+       <row>
+        <entry><type>pg_encrypted_det</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, deterministic encryption</entry>
+       </row>
+       <row>
+        <entry><type>pg_encrypted_rnd</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, randomized encryption</entry>
+       </row>
+      </tbody>
+     </tgroup>
+    </table>
+  </sect1>
  </chapter>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 5179125510..65514e119f 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,440 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   With <firstterm>automatic client-side column-level encryption</firstterm>,
+   columns can be stored encrypted in the database.  The encryption and
+   decryption happens automatically on the client, so that the plaintext value
+   is never seen in the database instance or on the server hosting the
+   database.  The drawback is that most operations, such as function calls or
+   sorting, are not possible on encrypted values.
+  </para>
+
+  <sect2>
+   <title>Using Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   Automatic client-side column-level encryption uses two levels of
+   cryptographic keys.  The actual column value is encrypted using a symmetric
+   algorithm, such as AES, using a <firstterm>column encryption
+   key</firstterm> (<acronym>CEK</acronym>).  The column encryption key is in
+   turn encrypted using an asymmetric algorithm, such as RSA, using a
+   <firstterm>column master key</firstterm> (<acronym>CMK</acronym>).  The
+   encrypted CEK is stored in the database system.  The CMK is not stored in
+   the database system; it is stored on the client or somewhere where the
+   client can access it, such as in a local file or in a key management
+   system.  The database system only records where the CMK is stored and
+   provides this information to the client.  When rows containing encrypted
+   columns are sent to the client, the server first sends any necessary CMK
+   information, followed by any required CEK.  The client then looks up the
+   CMK and uses that to decrypt the CEK.  Then it decrypts incoming row data
+   using the CEK and provides the decrypted row data to the application.
+  </para>
+
+  <para>
+   Here is an example declaring a column as encrypted:
+<programlisting>
+CREATE TABLE customers (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    creditcard_num text <emphasis>ENCRYPTED WITH (column_encryption_key = cek1)</emphasis>
+);
+</programlisting>
+  </para>
+
+  <para>
+   Column encryption supports <firstterm>randomized</firstterm>
+   (also known as <firstterm>probabilistic</firstterm>) and
+   <firstterm>deterministic</firstterm> encryption.  The above example uses
+   randomized encryption, which is the default.  Randomized encryption uses a
+   random initialization vector for each encryption, so that even if the
+   plaintext of two rows is equal, the encrypted values will be different.
+   This prevents someone with direct access to the database server from making
+   computations such as distinct counts on the encrypted values.
+   Deterministic encryption uses a fixed initialization vector.  This reduces
+   security, but it allows equality searches on encrypted values.  The
+   following example declares a column with deterministic encryption:
+<programlisting>
+CREATE TABLE employees (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    ssn text ENCRYPTED WITH (
+        column_encryption_key = cek1, <emphasis>encryption_type = deterministic</emphasis>)
+);
+</programlisting>
+  </para>
+
+  <para>
+   Null values are not encrypted by automatic client-side column-level
+   encryption; null values sent by the client are visible as null values in
+   the database.  If the fact that a value is null needs to be hidden from the
+   server, this information needs to be encoded into a nonnull value in the
+   client somehow.
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Reading and Writing Encrypted Columns</title>
+
+   <para>
+    Reading and writing encrypted columns is meant to be handled automatically
+    by the client library/driver and should be mostly transparent to the
+    application code, if certain prerequisites are fulfilled:
+
+    <itemizedlist>
+     <listitem>
+      <para>
+       The client library needs to support automatic client-side column-level
+       encryption.  Not all client libraries do.  Furthermore, the client
+       library might require that automatic client-side column-level
+       encryption is explicitly enabled at connection time.  See the
+       documentation of the client library for details.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Column master keys and column encryption keys have been set up, and the
+       client library has been configured to be able to look up column master
+       keys from the key store or key management system.
+      </para>
+     </listitem>
+    </itemizedlist>
+   </para>
+
+   <para>
+    Reading from encrypted columns will then work automatically.  For example,
+    using the above example,
+<programlisting>
+SELECT ssn FROM employees WHERE id = 5;
+</programlisting>
+    would return the unencrypted value for the <literal>ssn</literal> column
+    in any rows found.
+   </para>
+
+   <para>
+    Writing to encrypted columns requires that the extended query protocol
+    (protocol-level prepared statements) be used, so that the values to be
+    encrypted are supplied separately from the SQL command.  For example,
+    using, say, psql or libpq, the following would not work:
+<programlisting>
+-- WRONG!
+INSERT INTO employees (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This would leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of client-side column-level encryption.
+    (And even ignoring that, it could not work because the server does not
+    have access to the keys to perform the encryption.)  Note that using
+    server-side prepared statements using the SQL commands
+    <command>PREPARE</command> and <command>EXECUTE</command> is equally
+    incorrect, since that would also leak the parameters provided to
+    <command>EXECUTE</command> to the server.
+   </para>
+
+   <para>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</programlisting>
+    Higher-level client libraries might use the protocol-level prepared
+    statements automatically and thus won't require any code changes.
+   </para>
+
+   <para>
+    <application>psql</application> provides the command
+    <literal>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   The steps to set up automatic client-side column-level encryption for a
+   database are:
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK, for example, using a cryptographic
+      library or toolkit, or a key management system.  Secure access to the
+      key as appropriate, using access control, passwords, etc.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database using the SQL command <xref
+      linkend="sql-create-column-master-key"/>.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the (unencrypted) key material for the CEK in a temporary
+      location.  (It will be encrypted in the next step.  Depending on the
+      available tools, it might be possible and sensible to combine these two
+      steps.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material using the CMK (created earlier).
+      (The unencrypted version of the CEK key material can now be disposed
+      of.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database using the SQL command <xref
+      linkend="sql-create-column-encryption-key"/>.  This command
+      <quote>uploads</quote> the encrypted CEK key material created in the
+      previous step to the database server.  The local copy of the CEK key
+      material can then be removed.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns using the created CEK.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the client library/driver to be able to look up the CMK
+      created earlier.
+     </para>
+    </listitem>
+   </orderedlist>
+
+   Once this is done, values can be written to and read from the encrypted
+   columns in a transparent way.
+  </para>
+
+  <para>
+   Note that these steps should not be run on the database server, but on some
+   client machine.  Neither the CMK nor the unencrypted CEK should ever appear
+   on the database server host.
+  </para>
+
+  <para>
+   The specific details of this setup depend on the desired CMK storage
+   mechanism/key management system as well as the client libraries to be used.
+   The following example uses the <command>openssl</command> command-line tool
+   to set up the keys.
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK and write it to a file:
+<programlisting>
+openssl genpkey -algorithm rsa -out cmk1.pem
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database:
+<programlisting>
+psql ... -c "CREATE COLUMN MASTER KEY cmk1"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</programlisting>
+      (See <xref linkend="protocol-cek-table"/> for required key lengths.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material:
+<programlisting>
+openssl pkeyutl -encrypt -inkey cmk1.pem -pkeyopt rsa_padding_mode:oaep -in cek1.bin -out cek1.bin.enc
+rm cek1.bin
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database:
+<programlisting>
+# convert file contents to hex encoding; this is just one possible way
+cekenchex=$(perl -0ne 'print unpack "H*"' cek1.bin.enc)
+psql ... -c "CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\x${cekenchex}')"
+rm cek1.bin.enc
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns as shown in the examples above.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the libpq for CMK lookup (see also <xref linkend="libpq-connect-cmklookup"/>):
+<programlisting>
+PGCMKLOOKUP="*=file:$PWD/%k.pem"
+export PGCMKLOOKUP
+</programlisting>
+     </para>
+
+     <para>
+      Additionally, libpq requires that the connection parameter <xref
+      linkend="libpq-connect-column-encryption"/> be set in order to activate
+      the automatic client-side column-level encryption functionality.  This
+      should be done in the connection parameters of the application, but an
+      environment variable (<envar>PGCOLUMNENCRYPTION</envar>) is also
+      available.
+     </para>
+    </listitem>
+   </orderedlist>
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Guidance on Using Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use automatic client-side column-level encryption, and what precautions
+    need to be taken to maintain its security.
+   </para>
+
+   <para>
+    In general, column encryption is never a replacement for additional
+    security and encryption techniques such as transport encryption
+    (SSL/TLS), storage encryption, strong access control, and password
+    security.  Column encryption only targets specific use cases and should be
+    used in conjunction with additional security measures.
+   </para>
+
+   <para>
+    A typical use case for column encryption is to encrypt specific values
+    with additional security requirements, for example credit card numbers.
+    This allows you to store that security-sensitive data together with the
+    rest of your data (thus getting various benefits, such as referential
+    integrity, consistent backups), while giving access to that data only to
+    specific clients and preventing accidental leakage on the server side
+    (server logs, file system backups, etc.).
+   </para>
+
+   <para>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation; for
+    <application>libpq</application>, see <xref
+    linkend="libpq-connect-column-encryption"/>).
+   </para>
+
+   <para>
+    Column encryption cannot hide the existence or absence of data, it can
+    only disguise the particular data that is known to exist.  For example,
+    storing a cleartext person name and an encrypted credit card number
+    indicates that the person has a credit card.  That might not reveal too
+    much if the database is for an online store and there is other data nearby
+    that shows that the person has recently made purchases.  But in another
+    example, storing a cleartext person name and an encrypted diagnosis in a
+    medical database probably indicates that the person has a medical issue.
+    Depending on the circumstances, that might not by itself be sufficient
+    security.
+   </para>
+
+   <para>
+    Encryption cannot completely hide the length of values.  The encryption
+    methods will pad values to multiples of the underlying cipher's block size
+    (usually 16 bytes), so some length differences will be unified this way.
+    There is no concern if all values are of the same length, but if there are
+    signficant length differences between valid values and that length
+    information is security-sensitive, then application-specific workarounds
+    such as padding would need to be applied.  How to do that securely is
+    beyond the scope of this manual.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <tip>
+    <para>
+     One might be inclined to think of the client-side column-level encryption
+     feature as a mechanism for application writers and users to protect
+     themselves against an <quote>evil DBA</quote>, but that is not the
+     intended purpose.  Rather, it is (also) a tool for the DBA to control
+     which data they do not want (in plaintext) on the server.
+    </para>
+   </tip>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on is usually
+     subject to government or industry regulations.  This section is not meant
+     to provide complete instructions on how to do this correctly.  Please
+     seek additional advice when engaging in such projects.
+    </para>
+   </note>
+  </sect2>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
@@ -1986,6 +2420,14 @@ <title>Privileges</title>
        server.  Grantees may also create, alter, or drop their own user
        mappings associated with that server.
       </para>
+      <para>
+       For column master keys, allows the creation of column encryption keys
+       using the master key.
+      </para>
+      <para>
+       For column encryption keys, allows the use of the key in the creation
+       of table columns.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2152,6 +2594,8 @@ <title>ACL Privilege Abbreviations</title>
       <entry><literal>USAGE</literal></entry>
       <entry><literal>U</literal></entry>
       <entry>
+       <literal>COLUMN ENCRYPTION KEY</literal>,
+       <literal>COLUMN MASTER KEY</literal>,
        <literal>DOMAIN</literal>,
        <literal>FOREIGN DATA WRAPPER</literal>,
        <literal>FOREIGN SERVER</literal>,
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 97b3f1c1a6..dfaa6ff6d8 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22859,6 +22859,40 @@ <title>Access Privilege Inquiry Functions</title>
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_encryption_key_privilege</primary>
+        </indexterm>
+        <function>has_column_encryption_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cek</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column encryption key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_master_key_privilege</primary>
+        </indexterm>
+        <function>has_column_master_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cmk</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column master key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
@@ -23349,6 +23383,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 7c01a541fe..818038d860 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,32 @@ <title>Glossary</title>
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-column-encryption-key">
+   <glossterm>Column encryption key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt column values when using automatic
+     client-side column-level encryption (<xref
+     linkend="ddl-column-encryption"/>).  Column encryption keys are stored in
+     the database encrypted by another key, the <glossterm
+     linkend="glossary-column-master-key">column master key</glossterm>.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt <glossterm
+     linkend="glossary-column-encryption-key">column encryption
+     keys</glossterm>.  (So the column master key is a <firstterm>key
+     encryption key</firstterm>.)  Column master keys are stored outside the
+     database system, for example in a key management system.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-commit">
    <glossterm>Commit</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3ccd8ff942..26cab10104 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,141 @@ <title>Parameter Key Words</title>
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-connect-column-encryption" xreflabel="column_encryption">
+      <term><literal>column_encryption</literal></term>
+      <listitem>
+       <para>
+        If set to <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, this enables automatic client-side column-level
+        encryption for the connection.  If encrypted columns are queried and
+        this is not enabled, the encrypted values are returned.  See <xref
+        linkend="ddl-column-encryption"/> for more information about this
+        feature.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-cmklookup" xreflabel="cmklookup">
+      <term><literal>cmklookup</literal></term>
+      <listitem>
+       <para>
+        This specifies how libpq should look up column master keys (CMKs) in
+        order to decrypt the column encryption keys (CEKs).
+        The value is a list of <literal>key=value</literal> entries separated
+        by semicolons.  Each key is the name of a key realm, or
+        <literal>*</literal> to match all realms.  The value is a
+        <literal>scheme:data</literal> specification.  The scheme specifies
+        the method to look up the key, the remaining data is specific to the
+        scheme.  Placeholders are replaced in the remaining data as follows:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>%a</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name (see <xref linkend="protocol-cmk-table"/>)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%r</literal></term>
+          <listitem>
+           <para>
+            The realm name
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        Available schemes are:
+        <variablelist>
+         <varlistentry>
+          <term><literal>file</literal></term>
+          <listitem>
+           <para>
+            Load the key material from a file.  The remaining data is the file
+            name.  Use this if the CMKs are kept in a file on the file system.
+           </para>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>run</literal></term>
+          <listitem>
+           <para>
+            Run the specified command to decrypt the CEK.  The remaining data
+            is a shell command.  Use this with key management systems that
+            perform the decryption themselves.  The command must print the
+            decrypted plaintext on the standard output.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        The default value is empty.
+       </para>
+
+       <para>
+        Example:
+<programlisting>
+cmklookup="r1=file:/some/where/secrets/%k.pem;*=file:/else/where/%r/%k.pem"
+</programlisting>
+        This specification says, for keys in realm <quote>r1</quote>, load
+        them from the specified file, replacing <literal>%k</literal> by the
+        key name.  For keys in other realms, load them from the file,
+        replacing realm and key names as specified.
+       </para>
+
+       <para>
+        An example for interacting with a (hypothetical) key management
+        system:
+<programlisting>
+cmklookup="*=run:acmekms decrypt --key %k --alg %a --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2999,32 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            half-byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this half-byte to one to force
+            encryption.  For example, use the C code literal
+            <literal>0x10</literal> to specify text format with forced
+            encryption.  If the array pointer is null then encryption is not
+            forced for any parameter.
+           </para>
+
+           <para>
+            Parameters corresponding to encrypted columns must be passed in
+            text format.  Specifying binary format for such a parameter will
+            result in an error.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter does not
+            correspond to an encrypted column on the server, then the call
+            will fail and the parameter will not be sent.  This can be used
+            for additional security against a compromised server.  (The
+            drawback is that application code then needs to be kept up to date
+            with knowledge about which columns are encrypted rather than
+            letting the server specify this.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3037,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3196,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Sends a request to execute a prepared statement with given
+        parameters, and waits for the result, with support for encrypted columns.
+<synopsis>
+PGresult *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> is like <xref
+        linkend="libpq-PQexecPrepared"/> with additional support for encrypted
+        columns.  The parameter <parameter>paramDesc</parameter> must be a
+        result set obtained from <xref linkend="libpq-PQdescribePrepared"/> on
+        the same prepared statement.
+       </para>
+
+       <para>
+        This function must be used if a statement parameter corresponds to an
+        underlying encrypted column.  In that situation, the prepared
+        statement needs to be described first so that libpq can obtain the
+        necessary key and other information from the server.  When that is
+        done, the parameters corresponding to encrypted columns are
+        automatically encrypted appropriately before being sent to the server.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4084,28 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQfisencrypted">
+     <term><function>PQfisencrypted</function><indexterm><primary>PQfisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given column came from an encrypted
+       column.  Column numbers start at 0.
+<synopsis>
+int PQfisencrypted(const PGresult *res,
+                   int column_number);
+</synopsis>
+      </para>
+
+      <para>
+       Encrypted column values are automatically decrypted, so this function
+       is not necessary to access the column value.  It can be used for extra
+       security to check whether the value was stored encrypted when one
+       thought it should be.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQfsize">
      <term><function>PQfsize</function><indexterm><primary>PQfsize</primary></indexterm></term>
 
@@ -4059,6 +4287,31 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQparamisencrypted">
+     <term><function>PQparamisencrypted</function><indexterm><primary>PQparamisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given parameter is destined for an
+       encrypted column.  Parameter numbers start at 0.
+<synopsis>
+int PQparamisencrypted(const PGresult *res, int param_number);
+</synopsis>
+      </para>
+
+      <para>
+       Values for parameters destined for encrypted columns are automatically
+       encrypted, so this function is not necessary to prepare the parameter
+       value.  It can be used for extra security to check whether the value
+       will be stored encrypted when one thought it should be.  (But see also
+       at <xref linkend="libpq-PQexecPreparedDescribed"/> for another way to do that.)
+       This function is only useful when inspecting the result of <xref
+       linkend="libpq-PQdescribePrepared"/>.  For other types of results it
+       will return false.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQprint">
      <term><function>PQprint</function><indexterm><primary>PQprint</primary></indexterm></term>
 
@@ -4584,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4845,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4902,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4963,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Sends a request to execute a prepared statement with given
+       parameters, without waiting for the result(s), with support for encrypted columns.
+<synopsis>
+int PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> is like <xref
+       linkend="libpq-PQsendQueryPrepared"/> with additional support for encrypted
+       columns.  The parameter <parameter>paramDesc</parameter> must be a
+       result set obtained from <xref linkend="libpq-PQsendDescribePrepared"/> on
+       the same prepared statement.
+      </para>
+
+      <para>
+       This function must be used if a statement parameter corresponds to an
+       underlying encrypted column.  In that situation, the prepared
+       statement needs to be described first so that libpq can obtain the
+       necessary key and other information from the server.  When that is
+       done, the parameters corresponding to encrypted columns are
+       automatically encrypted appropriately before being sent to the server.
+       See also under <xref linkend="libpq-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5052,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8086,26 @@ <title>Environment Variables</title>
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCMKLOOKUP</envar></primary>
+      </indexterm>
+      <envar>PGCMKLOOKUP</envar> behaves the same as the <xref
+      linkend="libpq-connect-cmklookup"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCOLUMNENCRYPTION</envar></primary>
+      </indexterm>
+      <envar>PGCOLUMNENCRYPTION</envar> behaves the same as the <xref
+      linkend="libpq-connect-column-encryption"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 73b7f4432f..c43c3051c3 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,76 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-column-encryption">
+   <title>Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    Automatic client-side column-level encryption is enabled by sending the
+    parameter <literal>_pq_.column_encryption</literal> with a value of
+    <literal>1</literal> in the StartupMessage.  This is a protocol extension
+    that enables a few additional protocol messages and adds additional fields
+    to existing protocol messages.  Client drivers should only activate this
+    protocol extension when requested by the user, for example through a
+    connection parameter.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the
+    messages ColumnMasterKey and ColumnEncryptionKey can appear before
+    RowDescription and ParameterDescription messages.  Clients should collect
+    the information in these messages and keep them for the duration of the
+    connection.  A server is not required to resend the key information for
+    each statement cycle if it was already sent during this connection.  If a
+    server resends a key that the client has already stored (that is, a key
+    having an ID equal to one already stored), the new information should
+    replace the old.  (This could happen, for example, if the key was altered
+    by server-side DDL commands.)
+   </para>
+
+   <para>
+    A client supporting automatic column-level encryption should automatically
+    decrypt the column value fields of DataRow messages corresponding to
+    encrypted columns, and it should automatically encrypt the parameter value
+    fields of Bind messages corresponding to encrypted columns.
+   </para>
+
+   <para>
+    When column encryption is used, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column-level encryption protocol option is enabled.  That way, a client
+    library only needs to support decrypting data sent in binary and does not
+    have to support decoding the text format of the encryption-related types
+    (see <xref linkend="datatype-encrypted"/>).
+   </para>
+
+   <para>
+    When deterministic encryption is used, clients need to take care to
+    represent plaintext to be encrypted in a consistent form.  For example,
+    encrypting an integer represented by the string <literal>100</literal> and
+    an integer represented by the string <literal>+100</literal> would result
+    in two different ciphertexts, thus defeating the main point of
+    deterministic encryption.  This protocol specification requires the
+    plaintext to be in <quote>canonical</quote> form, which is the form that
+    is produced by the server when it outputs a particular value in text
+    format.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the client
+    encoding must match the server encoding.  This ensures that all values
+    encrypted or decrypted by the client match the server encoding.
+   </para>
+
+   <para>
+    The cryptographic operations used for automatic client-side column-level
+    encryption are described in <xref
+    linkend="protocol-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -3841,6 +3911,16 @@ <title>Message Formats</title>
          The parameter format codes.  Each must presently be
          zero (text) or one (binary).
         </para>
+
+        <para>
+         If the protocol extension <literal>_pq_.column_encryption</literal>
+         is enabled (see <xref linkend="protocol-flow-column-encryption"/>),
+         then the second-least-significant half-byte is set to one if the
+         parameter was encrypted by the client.  (So, for example, to send an
+         encrypted value in binary, the field is set to 0x11 in total.)  This
+         is used by the server to check that a parameter that was required to
+         be encrypted was actually encrypted.
+        </para>
        </listitem>
       </varlistentry>
 
@@ -4061,6 +4141,140 @@ <title>Message Formats</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ColumnEncryptionKey">
+    <term>ColumnEncryptionKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('Y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column encryption key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the master key used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the algorithm used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The length of the following key material.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Byte<replaceable>n</replaceable></term>
+       <listitem>
+        <para>
+         The key material, encrypted with the master key referenced above.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="protocol-message-formats-ColumnMasterKey">
+    <term>ColumnMasterKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column master key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The key's realm.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="protocol-message-formats-CommandComplete">
     <term>CommandComplete (B)</term>
     <listitem>
@@ -5164,6 +5378,45 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each parameter:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         This is used as a bit field of flags.  If the parameter is to be
+         encrypted and bit 0x0001 is set, the column underlying the parameter
+         uses deterministic encryption, otherwise randomized encryption.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5552,6 +5805,50 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each field:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         This is used as a bit field of flags.  If the field is encrypted and
+         bit 0x0001 is set, the field uses deterministic encryption, otherwise
+         randomized encryption.
+        </para>
+        <!--
+            This is not really useful here, but it keeps alignment with
+            ParameterDescription.  Future flags might be useful in both
+            places.
+        -->
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7377,6 +7674,176 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-column-encryption-crypto">
+  <title>Automatic Client-side Column-level Encryption Cryptography</title>
+
+  <para>
+   This section describes the cryptographic operations used by the automatic
+   client-side column-level encryption functionality.  A client that supports
+   this functionality needs to implement these operations as specified here in
+   order to be able to interoperate with other clients.
+  </para>
+
+  <para>
+   Column encryption key algorithms and column master key algorithms are
+   identified by integers in the protocol messages and the system catalogs.
+   Additional algorithms may be added to this protocol specification without a
+   change in the protocol version number.  Clients should implement support
+   for all the algorithms specified here.  If a client encounters an algorithm
+   identifier it does not recognize or does not support, it must raise an
+   error.  A suitable error message should be provided to the application or
+   user.
+  </para>
+
+  <sect2 id="protocol-cmk">
+   <title>Column Master Keys</title>
+
+   <para>
+    The currently defined algorithms for column master keys are listed in
+    <xref linkend="protocol-cmk-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC
+       8017</ulink>/PKCS #1)</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect2>
+
+  <sect2 id="protocol-cek">
+   <title>Column Encryption Keys</title>
+
+   <para>
+    The currently defined algorithms for column encryption keys are listed in
+    <xref linkend="protocol-cek-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</entry>
+       <entry><literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</entry>
+       <entry><literal>AEAD_AES_192_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_512</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <para>
+    The <quote>associated data</quote> in these algorithms consists of 4
+    bytes: The ASCII letters <literal>P</literal> and <literal>G</literal>
+    (byte values 80 and 71), followed by the version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </para>
+
+   <para>
+    The length of the initialization vector is 16 octets for all CEK algorithm
+    variants.  For randomized encryption, the initialization vector should be
+    (cryptographically strong) random bytes.  For deterministic encryption,
+    the initialization vector is constructed as
+<programlisting>
+SUBSTRING(<replaceable>HMAC</replaceable>(<replaceable>K</replaceable>, <replaceable>P</replaceable>) FOR <replaceable>IVLEN</replaceable>)
+</programlisting>
+    where <replaceable>HMAC</replaceable> is the HMAC function associated with
+    the algorithm, <replaceable>K</replaceable> is the IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </para>
+  </sect2>
+ </sect1>
+
  <sect1 id="protocol-changes">
   <title>Summary of Changes since Protocol 2.0</title>
 
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 54b5f22d6e..a730e5d650 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -8,6 +8,8 @@
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
+<!ENTITY alterColumnEncryptionKey SYSTEM "alter_column_encryption_key.sgml">
+<!ENTITY alterColumnMasterKey SYSTEM "alter_column_master_key.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
 <!ENTITY alterDefaultPrivileges SYSTEM "alter_default_privileges.sgml">
@@ -62,6 +64,8 @@
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
+<!ENTITY createColumnEncryptionKey SYSTEM "create_column_encryption_key.sgml">
+<!ENTITY createColumnMasterKey SYSTEM "create_column_master_key.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
 <!ENTITY createDatabase     SYSTEM "create_database.sgml">
 <!ENTITY createDomain       SYSTEM "create_domain.sgml">
@@ -109,6 +113,8 @@
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
+<!ENTITY dropColumnEncryptionKey SYSTEM "drop_column_encryption_key.sgml">
+<!ENTITY dropColumnMasterKey SYSTEM "drop_column_master_key.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
 <!ENTITY dropDatabase       SYSTEM "drop_database.sgml">
 <!ENTITY dropDomain         SYSTEM "drop_domain.sgml">
diff --git a/doc/src/sgml/ref/alter_column_encryption_key.sgml b/doc/src/sgml/ref/alter_column_encryption_key.sgml
new file mode 100644
index 0000000000..655e1e00d8
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+doc/src/sgml/ref/alter_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-encryption-key">
+ <indexterm zone="sql-alter-column-encryption-key">
+  <primary>ALTER COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>change the definition of a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> ADD VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> DROP VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN ENCRYPTION KEY</command> changes the definition of a
+   column encryption key.
+  </para>
+
+  <para>
+   The first form adds new encrypted key data to a column encryption key,
+   which must be encrypted with a different column master key than the
+   existing key data.  The second form removes a key data entry for a given
+   column master key.  Together, these forms can be used for column master key
+   rotation.
+  </para>
+
+  <para>
+   You must own the column encryption key to use <command>ALTER COLUMN
+   ENCRYPTION KEY</command>.  To alter the owner, you must also be a direct or
+   indirect member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column encryption key's
+   schema.  (These restrictions enforce that altering the owner doesn't do
+   anything you couldn't do by dropping and recreating the column encryption
+   key.  However, a superuser can alter ownership of any column encryption key
+   anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column encryption
+      key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  See <xref
+      linkend="sql-create-column-encryption-key"/> for details
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rotate the master keys used to encrypt a given column encryption key,
+   use a command sequence like this:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (
+    COLUMN_MASTER_KEY = cmk1
+);
+</programlisting>
+  </para>
+
+  <para>
+   To rename the column encryption key <literal>cek1</literal> to
+   <literal>cek2</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column encryption key <literal>cek1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN ENCRYPTION KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/alter_column_master_key.sgml b/doc/src/sgml/ref/alter_column_master_key.sgml
new file mode 100644
index 0000000000..7f0e656ef0
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+doc/src/sgml/ref/alter_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-master-key">
+ <indexterm zone="sql-alter-column-master-key">
+  <primary>ALTER COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN MASTER KEY</refname>
+  <refpurpose>change the definition of a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> ( REALM = <replaceable>realm</replaceable> )
+
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </para>
+
+  <para>
+   You must own the column master key to use <command>ALTER COLUMN MASTER
+   KEY</command>.  To alter the owner, you must also be a direct or indirect
+   member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column master key's schema.
+   (These restrictions enforce that altering the owner doesn't do anything you
+   couldn't do by dropping and recreating the column master key.  However, a
+   superuser can alter ownership of any column master key anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the column master key <literal>cmk1</literal> to
+   <literal>cmk2</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column master key <literal>cmk1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN MASTER KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..1caf9bfa56 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..ebcbf5d00a 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,16 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support automatic client-side
+    column-level encryption or decryption; its input or output data will
+    always be the ciphertext.  This is usually suitable for backups (see also
+    <xref linkend="app-pgdump"/>).  If automatic client-side encryption or
+    decryption is wanted, <command>INSERT</command> and
+    <command>SELECT</command> need to be used instead to write and read the
+    data.
+   </para>
+
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/ref/create_column_encryption_key.sgml b/doc/src/sgml/ref/create_column_encryption_key.sgml
new file mode 100644
index 0000000000..65534fb03f
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,173 @@
+<!--
+doc/src/sgml/ref/create_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-encryption-key">
+ <indexterm zone="sql-create-column-encryption-key">
+  <primary>CREATE COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>define a new column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN ENCRYPTION KEY <replaceable>name</replaceable> WITH VALUES (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    ALGORITHM = <replaceable>algorithm</replaceable>,
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+[ , ... ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN ENCRYPTION KEY</command> defines a new column
+   encryption key.  A column encryption key is used for client-side encryption
+   of table columns that have been defined as encrypted.  The key material of
+   a column encryption key is stored in the database's system catalogs,
+   encrypted (wrapped) by a column master key (which in turn is only
+   accessible to the client, not the database server).
+  </para>
+
+  <para>
+   A column encryption key can be associated with more than one column master
+   key.  To specify that, specify more than one parenthesized definition (see
+   also the examples).
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column encryption key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.  You must have <literal>USAGE</literal> privilege on the
+      column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  Supported algorithms are:
+      <itemizedlist>
+       <listitem>
+        <para><literal>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></para>
+       </listitem>
+      </itemizedlist>
+     </para>
+
+     <para>
+      This is informational only.  The specified value is provided to the
+      client, which may use it for decrypting the column encryption key on the
+      client side.  But a client is also free to ignore this information and
+      figure out how to arrange the decryption in some other way.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+</programlisting>
+  </para>
+
+  <para>
+   To specify more than one associated column master key:
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\xF1F2F2F4...'
+);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_column_master_key.sgml b/doc/src/sgml/ref/create_column_master_key.sgml
new file mode 100644
index 0000000000..6aaa1088d1
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+doc/src/sgml/ref/create_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-master-key">
+ <indexterm zone="sql-create-column-master-key">
+  <primary>CREATE COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN MASTER KEY</refname>
+  <refpurpose>define a new column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN MASTER KEY <replaceable>name</replaceable> [ WITH (
+    [ REALM = <replaceable>realm</replaceable> ]
+) ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN MASTER KEY</command> defines a new column master
+   key.  A column master key is used to encrypt column encryption keys, which
+   are the keys that actually encrypt the column data.  The key material of
+   the column master key is not stored in the database.  The definition of a
+   column master key records information that will allow a client to locate
+   the key material, for example in a file or in a key management system.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column master key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>realm</replaceable></term>
+
+    <listitem>
+     <para>
+      This is an optional string that can be used to organize column master
+      keys into groups for lookup by clients.  The intent is that all column
+      master keys that are stored in the same system (file system location,
+      key management system, etc.) should be in the same realm.  A client
+      would then be configured to look up all keys in a given realm in a
+      certain way.  See the documentation of the respective client library for
+      further usage instructions.
+     </para>
+
+     <para>
+      The default is the empty string.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+<programlisting>
+CREATE COLUMN MASTER KEY cmk1 (realm = 'myrealm');
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index a03dee4afe..d1549c7f45 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -22,7 +22,7 @@
  <refsynopsisdiv>
 <synopsis>
 CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable> ( [
-  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,47 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables automatic client-side column-level encryption for the column.
+      <replaceable>encryption_options</replaceable> are comma-separated
+      <literal>key=value</literal> specifications.  The following options are
+      available:
+      <variablelist>
+       <varlistentry>
+        <term><literal>column_encryption_key</literal></term>
+        <listitem>
+         <para>
+          Specifies the name of the column encryption key to use.  Specifying
+          this is mandatory.  You must have <literal>USAGE</literal> privilege
+          on the column encryption key.
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>encryption_type</literal></term>
+        <listitem>
+         <para>
+          <literal>randomized</literal> (the default) or <literal>deterministic</literal>
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>algorithm</literal></term>
+        <listitem>
+         <para>
+          The encryption algorithm to use.  The default is
+          <literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -704,6 +745,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</literal></term>
         <listitem>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523ca..6a94706ef7 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@
 
  <refsynopsisdiv>
 <synopsis>
-DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
+DISCARD { ALL | COLUMN ENCRYPTION KEYS | PLANS | SEQUENCES | TEMPORARY | TEMP }
 </synopsis>
  </refsynopsisdiv>
 
@@ -42,6 +42,17 @@ <title>Parameters</title>
 
   <variablelist>
 
+   <varlistentry>
+    <term><literal>COLUMN ENCRYPTION KEYS</literal></term>
+    <listitem>
+     <para>
+      Discards knowledge about which column encryption keys and column master
+      keys have been sent to the client in this session.  (They will
+      subsequently be re-sent as required.)
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>PLANS</literal></term>
     <listitem>
@@ -93,6 +104,7 @@ <title>Parameters</title>
 DISCARD PLANS;
 DISCARD TEMP;
 DISCARD SEQUENCES;
+DISCARD COLUMN ENCRYPTION KEYS;
 </programlisting></para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/ref/drop_column_encryption_key.sgml b/doc/src/sgml/ref/drop_column_encryption_key.sgml
new file mode 100644
index 0000000000..f2ac1beb08
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_encryption_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-encryption-key">
+ <indexterm zone="sql-drop-column-encryption-key">
+  <primary>DROP COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>remove a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN ENCRYPTION KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN ENCRYPTION KEY</command> removes a previously defined
+   column encryption key.  To be able to drop a column encryption key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column encryption key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column encryption key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column encryption key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column encryption key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN ENCRYPTION KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_column_master_key.sgml b/doc/src/sgml/ref/drop_column_master_key.sgml
new file mode 100644
index 0000000000..fae95e09d1
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_master_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-master-key">
+ <indexterm zone="sql-drop-column-master-key">
+  <primary>DROP COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN MASTER KEY</refname>
+  <refpurpose>remove a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN MASTER KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN MASTER KEY</command> removes a previously defined
+   column master key.  To be able to drop a column master key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column master key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column master key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column master key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column master key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN MASTER KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 35bf0332c8..f712f8e9e4 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -46,6 +46,16 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN ENCRYPTION KEY <replaceable>cek_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN MASTER KEY <replaceable>cmk_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON DOMAIN <replaceable>domain_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -513,7 +523,7 @@ <title>Compatibility</title>
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
+    Privileges on databases, tablespaces, schemas, keys, languages, and
     configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 49d218905f..536f10def0 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -716,6 +716,48 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option turns on the column encryption connection option in
+        <application>libpq</application> (see <xref
+        linkend="libpq-connect-column-encryption"/>).  Column master key
+        lookup must be configured by the user, either through a connection
+        option or an environment setting (see <xref
+        linkend="libpq-connect-cmklookup"/>).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  (But then it is recommended
+        to not do this on the same host as the server, to avoid exposing
+        unencrypted data that is meant to be kept encrypted on the server.)
+        Note that a dump created with this option cannot be restored into a
+        database with column encryption.
+       </para>
+       <!--
+           XXX: The latter would require another pg_dump option to put out
+           INSERT ... \bind ... \g commands.
+       -->
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index e62d05e5ab..4bf60c729f 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -259,6 +259,33 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  Note that a dump created
+        with this option cannot be restored into a database with column
+        encryption.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index dc6528dc11..e68b8440be 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4026,6 +4054,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <term><varname>HIDE_COLUMN_ENCRYPTION</varname></term>
+        <listitem>
+        <para>
+         If this variable is set to <literal>true</literal>, column encryption
+         details are not displayed. This is mainly useful for regression
+         tests.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..c898997915 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -36,6 +36,8 @@ <title>SQL Commands</title>
    &abort;
    &alterAggregate;
    &alterCollation;
+   &alterColumnEncryptionKey;
+   &alterColumnMasterKey;
    &alterConversion;
    &alterDatabase;
    &alterDefaultPrivileges;
@@ -90,6 +92,8 @@ <title>SQL Commands</title>
    &createAggregate;
    &createCast;
    &createCollation;
+   &createColumnEncryptionKey;
+   &createColumnMasterKey;
    &createConversion;
    &createDatabase;
    &createDomain;
@@ -137,6 +141,8 @@ <title>SQL Commands</title>
    &dropAggregate;
    &dropCast;
    &dropCollation;
+   &dropColumnEncryptionKey;
+   &dropColumnMasterKey;
    &dropConversion;
    &dropDatabase;
    &dropDomain;
diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index ef818228ac..c5894893eb 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -20,7 +20,9 @@
 
 #include "access/printsimple.h"
 #include "catalog/pg_type.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 
 /*
@@ -46,6 +48,12 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc)
 		pq_sendint16(&buf, attr->attlen);
 		pq_sendint32(&buf, attr->atttypmod);
 		pq_sendint16(&buf, 0);	/* format code */
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&buf, 0);	/* CEK */
+			pq_sendint32(&buf, 0);	/* CEK alg */
+			pq_sendint16(&buf, 0);	/* flags */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 72faeb5dfa..2d627b6f47 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,28 @@
  */
 #include "postgres.h"
 
+#include "access/genam.h"
 #include "access/printtup.h"
+#include "access/skey.h"
+#include "access/table.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
 #include "libpq/libpq.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memdebug.h"
 #include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
 
 
 static void printtup_startup(DestReceiver *self, int operation,
@@ -151,6 +166,166 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 	 */
 }
 
+/*
+ * Send ColumnMasterKey message, unless it's already been sent in this session
+ * for this key.
+ */
+List	   *cmk_sent = NIL;
+
+static void
+cmk_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+}
+
+static void
+MaybeSendColumnMasterKeyMessage(Oid cmkid)
+{
+	HeapTuple	tuple;
+	Form_pg_colmasterkey cmkform;
+	Datum		datum;
+	bool		isnull;
+	StringInfoData buf;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cmk_sent, cmkid))
+		return;
+
+	tuple = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	cmkform = (Form_pg_colmasterkey) GETSTRUCT(tuple);
+
+	pq_beginmessage(&buf, 'y'); /* ColumnMasterKey */
+	pq_sendint32(&buf, cmkform->oid);
+	pq_sendstring(&buf, NameStr(cmkform->cmkname));
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkrealm, &isnull);
+	Assert(!isnull);
+	pq_sendstring(&buf, TextDatumGetCString(datum));
+	pq_endmessage(&buf);
+
+	ReleaseSysCache(tuple);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CMKOID, cmk_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cmk_sent = lappend_oid(cmk_sent, cmkid);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * Send ColumnEncryptionKey message, unless it's already been sent in this
+ * session for this key.
+ */
+List	   *cek_sent = NIL;
+
+static void
+cek_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
+void
+MaybeSendColumnEncryptionKeyMessage(Oid attcek)
+{
+	HeapTuple	tuple;
+	ScanKeyData skey[1];
+	SysScanDesc sd;
+	Relation	rel;
+	bool		found = false;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cek_sent, attcek))
+		return;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	ScanKeyInit(&skey[0],
+				Anum_pg_colenckeydata_ckdcekid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(attcek));
+	rel = table_open(ColumnEncKeyDataRelationId, AccessShareLock);
+	sd = systable_beginscan(rel, ColumnEncKeyCekidCmkidIndexId, true, NULL, 1, skey);
+
+	while ((tuple = systable_getnext(sd)))
+	{
+		Form_pg_colenckeydata ckdform = (Form_pg_colenckeydata) GETSTRUCT(tuple);
+		Datum		datum;
+		bool		isnull;
+		bytea	   *ba;
+		StringInfoData buf;
+
+		MaybeSendColumnMasterKeyMessage(ckdform->ckdcmkid);
+
+		datum = heap_getattr(tuple, Anum_pg_colenckeydata_ckdencval, RelationGetDescr(rel), &isnull);
+		Assert(!isnull);
+		ba = pg_detoast_datum_packed((bytea *) DatumGetPointer(datum));
+
+		pq_beginmessage(&buf, 'Y'); /* ColumnEncryptionKey */
+		pq_sendint32(&buf, ckdform->ckdcekid);
+		pq_sendint32(&buf, ckdform->ckdcmkid);
+		pq_sendint32(&buf, ckdform->ckdcmkalg);
+		pq_sendint32(&buf, VARSIZE_ANY_EXHDR(ba));
+		pq_sendbytes(&buf, VARDATA_ANY(ba), VARSIZE_ANY_EXHDR(ba));
+		pq_endmessage(&buf);
+
+		found = true;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	systable_endscan(sd);
+	table_close(rel, NoLock);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CEKDATAOID, cek_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cek_sent = lappend_oid(cek_sent, attcek);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+void
+DiscardColumnEncryptionKeys(void)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -167,6 +342,7 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 						  List *targetlist, int16 *formats)
 {
 	int			natts = typeinfo->natts;
+	size_t		sz;
 	int			i;
 	ListCell   *tlist_item = list_head(targetlist);
 
@@ -183,14 +359,18 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 	 * Have to overestimate the size of the column-names, to account for
 	 * character set overhead.
 	 */
-	enlargeStringInfo(buf, (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
-							+ sizeof(Oid)	/* resorigtbl */
-							+ sizeof(AttrNumber)	/* resorigcol */
-							+ sizeof(Oid)	/* atttypid */
-							+ sizeof(int16) /* attlen */
-							+ sizeof(int32) /* attypmod */
-							+ sizeof(int16) /* format */
-							) * natts);
+	sz = (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
+		  + sizeof(Oid)	/* resorigtbl */
+		  + sizeof(AttrNumber)	/* resorigcol */
+		  + sizeof(Oid)	/* atttypid */
+		  + sizeof(int16) /* attlen */
+		  + sizeof(int32) /* attypmod */
+		  + sizeof(int16)); /* format */
+	if (MyProcPort->column_encryption_enabled)
+		sz += (sizeof(int32)		/* attcekid */
+			   + sizeof(int32)		/* attencalg */
+			   + sizeof(int16));	/* flags */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +380,9 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
+		int16		flags = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +414,31 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(atttypid))
+		{
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(resorigtbl), Int16GetDatum(resorigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", resorigcol, resorigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			atttypid = orig_att->attusertypid;
+			atttypmod = orig_att->attusertypmod;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->atttypmod;
+			if (orig_att->atttypid == PG_ENCRYPTED_DETOID)
+				flags |= 0x0001;
+			ReleaseSysCache(tp);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +446,12 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		pq_writeint16(buf, att->attlen);
 		pq_writeint32(buf, atttypmod);
 		pq_writeint16(buf, format);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_writeint32(buf, attcekid);
+			pq_writeint32(buf, attencalg);
+			pq_writeint16(buf, flags);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +485,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 72a2c3d3db..f86ba299c3 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attusertypid != attr2->attusertypid)
+			return false;
+		if (attr1->attusertypmod != attr2->attusertypmod)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 24bab58499..cc48069932 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index a60107bf94..7b9575635b 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index c4232344aa..a3547c6cae 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_default_acl.h"
@@ -247,6 +249,12 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_SEQUENCE:
 			whole_mask = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			whole_mask = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			whole_mask = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			whole_mask = ACL_ALL_RIGHTS_DATABASE;
 			break;
@@ -473,6 +481,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
 			errormsg = gettext_noop("invalid privilege type %s for sequence");
 			break;
+		case OBJECT_CEK:
+			all_privileges = ACL_ALL_RIGHTS_CEK;
+			errormsg = gettext_noop("invalid privilege type %s for column encryption key");
+			break;
+		case OBJECT_CMK:
+			all_privileges = ACL_ALL_RIGHTS_CMK;
+			errormsg = gettext_noop("invalid privilege type %s for column master key");
+			break;
 		case OBJECT_DATABASE:
 			all_privileges = ACL_ALL_RIGHTS_DATABASE;
 			errormsg = gettext_noop("invalid privilege type %s for database");
@@ -597,6 +613,12 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_SEQUENCE:
 			ExecGrant_Relation(istmt);
 			break;
+		case OBJECT_CEK:
+			ExecGrant_common(istmt, ColumnEncKeyRelationId, ACL_ALL_RIGHTS_CEK, NULL);
+			break;
+		case OBJECT_CMK:
+			ExecGrant_common(istmt, ColumnMasterKeyRelationId, ACL_ALL_RIGHTS_CMK, NULL);
+			break;
 		case OBJECT_DATABASE:
 			ExecGrant_common(istmt, DatabaseRelationId, ACL_ALL_RIGHTS_DATABASE, NULL);
 			break;
@@ -676,6 +698,26 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 				objects = lappend_oid(objects, relOid);
 			}
 			break;
+		case OBJECT_CEK:
+			foreach(cell, objnames)
+			{
+				List	   *cekname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cek_oid(cekname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
+		case OBJECT_CMK:
+			foreach(cell, objnames)
+			{
+				List	   *cmkname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cmk_oid(cmkname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
 		case OBJECT_DATABASE:
 			foreach(cell, objnames)
 			{
@@ -2693,6 +2735,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("permission denied for column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("permission denied for column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -2798,6 +2846,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -2828,6 +2877,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("must be owner of aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("must be owner of column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("must be owner of column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("must be owner of collation %s");
 						break;
@@ -2938,6 +2993,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3019,6 +3075,10 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
+		case OBJECT_CEK:
+			return object_aclmask(ColumnEncKeyRelationId, object_oid, roleid, mask, how);
+		case OBJECT_CMK:
+			return object_aclmask(ColumnMasterKeyRelationId, object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
 		case OBJECT_FUNCTION:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..cab6cfd140 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -30,7 +30,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -153,6 +156,9 @@ static const Oid object_classes[] = {
 	TypeRelationId,				/* OCLASS_TYPE */
 	CastRelationId,				/* OCLASS_CAST */
 	CollationRelationId,		/* OCLASS_COLLATION */
+	ColumnEncKeyRelationId,		/* OCLASS_CEK */
+	ColumnEncKeyDataRelationId,	/* OCLASS_CEKDATA */
+	ColumnMasterKeyRelationId,	/* OCLASS_CMK */
 	ConstraintRelationId,		/* OCLASS_CONSTRAINT */
 	ConversionRelationId,		/* OCLASS_CONVERSION */
 	AttrDefaultRelationId,		/* OCLASS_DEFAULT */
@@ -1493,6 +1499,9 @@ doDeletion(const ObjectAddress *object, int flags)
 
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONVERSION:
 		case OCLASS_LANGUAGE:
 		case OCLASS_OPCLASS:
@@ -2859,6 +2868,15 @@ getObjectClass(const ObjectAddress *object)
 		case CollationRelationId:
 			return OCLASS_COLLATION;
 
+		case ColumnEncKeyRelationId:
+			return OCLASS_CEK;
+
+		case ColumnEncKeyDataRelationId:
+			return OCLASS_CEKDATA;
+
+		case ColumnMasterKeyRelationId:
+			return OCLASS_CMK;
+
 		case ConstraintRelationId:
 			return OCLASS_CONSTRAINT;
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 4f006820b8..df282c796f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypid - 1] = ObjectIdGetDatum(attrs->attusertypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypmod - 1] = Int32GetDatum(attrs->attusertypmod);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attusertypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attusertypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 14e57adee2..00f914bc5f 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -26,6 +26,8 @@
 #include "catalog/dependency.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1997,6 +1999,254 @@ OpfamilyIsVisible(Oid opfid)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4567,6 +4817,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(OpfamilyIsVisible(oid));
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2f688166e1..6c4ab9ac59 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -191,6 +194,48 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_COLLATION,
 		true
 	},
+	{
+		"column encryption key",
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		CEKOID,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		Anum_pg_colenckey_cekowner,
+		Anum_pg_colenckey_cekacl,
+		OBJECT_CEK,
+		true
+	},
+	{
+		"column encryption key data",
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		CEKDATAOID,
+		-1,
+		Anum_pg_colenckeydata_oid,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		-1,
+		false
+	},
+	{
+		"column master key",
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		CMKOID,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		Anum_pg_colmasterkey_cmkowner,
+		Anum_pg_colmasterkey_cmkacl,
+		OBJECT_CMK,
+		true
+	},
 	{
 		"constraint",
 		ConstraintRelationId,
@@ -723,6 +768,18 @@ static const struct object_type_map
 	{
 		"collation", OBJECT_COLLATION
 	},
+	/* OCLASS_CEK */
+	{
+		"column encryption key", OBJECT_CEK
+	},
+	/* OCLASS_CEKDATA */
+	{
+		"column encryption key data", OBJECT_CEKDATA
+	},
+	/* OCLASS_CMK */
+	{
+		"column master key", OBJECT_CMK
+	},
 	/* OCLASS_CONSTRAINT */
 	{
 		"table constraint", OBJECT_TABCONSTRAINT
@@ -1029,6 +1086,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1108,6 +1175,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, castNode(List, object));
+					Oid			cekid;
+					Oid			cmkid;
+
+					cekid = get_cek_oid(cekname, missing_ok);
+					cmkid = get_cmk_oid(cmkname, missing_ok);
+
+					address.classId = ColumnEncKeyDataRelationId;
+					address.objectId = get_cekdata_oid(cekid, cmkid, missing_ok);
+					address.objectSubId = 0;
+				}
+				break;
 			case OBJECT_TRANSFORM:
 				{
 					TypeName   *typename = linitial_node(TypeName, castNode(List, object));
@@ -2311,6 +2393,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2367,6 +2451,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2489,6 +2574,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2575,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3040,6 +3128,92 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CEKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata cekdata;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+
+				cekdata = (Form_pg_colenckeydata) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("column encryption key data of %s for %s"),
+								 getObjectDescription(&(ObjectAddress){ColumnEncKeyRelationId, cekdata->ckdcekid}, false),
+								 getObjectDescription(&(ObjectAddress){ColumnMasterKeyRelationId, cekdata->ckdcmkid}, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4440,6 +4614,18 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "collation");
 			break;
 
+		case OCLASS_CEK:
+			appendStringInfoString(&buffer, "column encryption key");
+			break;
+
+		case OCLASS_CEKDATA:
+			appendStringInfoString(&buffer, "column encryption key data");
+			break;
+
+		case OCLASS_CMK:
+			appendStringInfoString(&buffer, "column master key");
+			break;
+
 		case OCLASS_CONSTRAINT:
 			getConstraintTypeDescription(&buffer, object->objectId,
 										 missing_ok);
@@ -4905,6 +5091,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata form;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckeydata) GETSTRUCT(tup);
+				appendStringInfo(&buffer,
+								 "of %s for %s",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column master key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91..69f6175c60 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -19,6 +19,7 @@ OBJS = \
 	analyze.o \
 	async.o \
 	cluster.o \
+	colenccmds.o \
 	collationcmds.o \
 	comment.o \
 	constraint.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index bea51b3af1..59c23e9ef8 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -22,7 +22,9 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_foreign_data_wrapper.h"
@@ -118,6 +120,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -379,6 +387,8 @@ ExecRenameStmt(RenameStmt *stmt)
 			return RenameType(stmt);
 
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_EVENT_TRIGGER:
@@ -527,6 +537,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -643,6 +655,9 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 			break;
 
 		case OCLASS_CAST:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
@@ -876,6 +891,8 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/colenccmds.c b/src/backend/commands/colenccmds.c
new file mode 100644
index 0000000000..3ada6d5aeb
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,439 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/colenccmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+static void
+parse_cek_attributes(ParseState *pstate, List *definition, Oid *cmkoid_p, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	Assert(cmkoid_p);
+
+	foreach(lc, definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "column_master_key") == 0)
+			defelp = &cmkEl;
+		else if (strcmp(defel->defname, "algorithm") == 0)
+			defelp = &algEl;
+		else if (strcmp(defel->defname, "encrypted_value") == 0)
+			defelp = &encvalEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column encryption key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (cmkEl)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("attribute \"%s\" must be specified",
+						"column_master_key")));
+
+	if (algEl)
+	{
+		char	   *val = defGetString(algEl);
+
+		if (!alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"algorithm")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	if (encvalEl)
+	{
+		if (!encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"encrypted_value")));
+
+		encval = defGetString(encvalEl);
+	}
+	else
+	{
+		if (encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"encrypted_value")));
+	}
+
+	*cmkoid_p = cmkoid;
+	if (alg_p)
+		*alg_p = alg;
+	if (encval_p)
+		*encval_p = encval;
+}
+
+static void
+insert_cekdata_record(Oid cekoid, Oid cmkoid, int alg, char *encval)
+{
+	Oid			cekdataoid;
+	Relation	rel;
+	Datum		values[Natts_pg_colenckeydata] = {0};
+	bool		nulls[Natts_pg_colenckeydata] = {0};
+	HeapTuple	tup;
+	ObjectAddress myself;
+	ObjectAddress other;
+
+	rel = table_open(ColumnEncKeyDataRelationId, RowExclusiveLock);
+
+	cekdataoid = GetNewOidWithIndex(rel, ColumnEncKeyDataOidIndexId, Anum_pg_colenckeydata_oid);
+	values[Anum_pg_colenckeydata_oid - 1] = ObjectIdGetDatum(cekdataoid);
+	values[Anum_pg_colenckeydata_ckdcekid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckeydata_ckdcmkid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colenckeydata_ckdcmkalg - 1] = Int32GetDatum(alg);
+	values[Anum_pg_colenckeydata_ckdencval - 1] = DirectFunctionCall1(byteain, CStringGetDatum(encval));
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyDataRelationId, cekdataoid);
+
+	/* dependency cekdata -> cek */
+	ObjectAddressSet(other, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_AUTO);
+
+	/* dependency cekdata -> cmk */
+	ObjectAddressSet(other, ColumnMasterKeyRelationId, cmkoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_NORMAL);
+
+	table_close(rel, NoLock);
+}
+
+ObjectAddress
+CreateCEK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+	nulls[Anum_pg_colenckey_cekacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOnOwner(ColumnEncKeyRelationId, cekoid, GetUserId());
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnEncKeyRelationId, cekoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt)
+{
+	Oid			cekoid;
+	ObjectAddress address;
+
+	cekoid = get_cek_oid(stmt->cekname, false);
+
+	if (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(stmt->cekname));
+
+	if (stmt->isDrop)
+	{
+		Oid			cmkoid = 0;
+		Oid			cekdataoid;
+		ObjectAddress obj;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, NULL, NULL);
+		cekdataoid = get_cekdata_oid(cekoid, cmkoid, false);
+		ObjectAddressSet(obj, ColumnEncKeyDataRelationId, cekdataoid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+		AclResult	aclresult;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		if (get_cekdata_oid(cekoid, cmkoid, true))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("column encryption key \"%s\" already has data for master key \"%s\"",
+						   NameListToString(stmt->cekname), get_cmk_name(cmkoid, false)));
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	InvokeObjectPostAlterHook(ColumnEncKeyRelationId, cekoid, 0);
+	ObjectAddressSet(address, ColumnEncKeyRelationId, cekoid);
+
+	return address;
+}
+
+ObjectAddress
+CreateCMK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+		realm = defGetString(realmEl);
+	else
+		realm = "";
+
+	cmkoid = GetNewOidWithIndex(rel, ColumnMasterKeyOidIndexId, Anum_pg_colmasterkey_oid);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+	nulls[Anum_pg_colmasterkey_cmkacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	recordDependencyOnOwner(ColumnMasterKeyRelationId, cmkoid, GetUserId());
+
+	ObjectAddressSet(myself, ColumnMasterKeyRelationId, cmkoid);
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d6c6d514f3..9685b7886a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 296dc82d2e..86d22ca065 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/printtup.h"
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "commands/async.h"
@@ -25,7 +26,7 @@
 static void DiscardAll(bool isTopLevel);
 
 /*
- * DISCARD { ALL | SEQUENCES | TEMP | PLANS }
+ * DISCARD
  */
 void
 DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
@@ -36,6 +37,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			DiscardAll(isTopLevel);
 			break;
 
+		case DISCARD_COLUMN_ENCRYPTION_KEYS:
+			DiscardColumnEncryptionKeys();
+			break;
+
 		case DISCARD_PLANS:
 			ResetPlanCache();
 			break;
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	DiscardColumnEncryptionKeys();
 }
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 82bda15889..b4c681005a 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +517,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..4c1628cf7b 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,9 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -1027,6 +1030,9 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_TYPE:
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_CONVERSION:
 		case OCLASS_DEFAULT:
@@ -2056,6 +2062,9 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2139,6 +2148,9 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 42cced9ebe..4b5ac30441 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..93d61c96ad 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,9 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 62d9917ca3..c30b59265b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,16 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			AclResult	aclresult;
+
+			GetColumnEncryption(colDef->encryption, attr);
+			aclresult = object_aclcheck(ColumnEncKeyRelationId, attr->attcek, GetUserId(), ACL_USAGE);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attr->attcek, false));
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -2562,13 +2575,43 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				Oid			defCollId;
 
 				/*
-				 * Yes, try to merge the two column definitions. They must
-				 * have the same type, typmod, and collation.
+				 * Yes, try to merge the two column definitions.
 				 */
 				ereport(NOTICE,
 						(errmsg("merging multiple inherited definitions of column \"%s\"",
 								attributeName)));
 				def = (ColumnDef *) list_nth(inhSchema, exist_attno - 1);
+
+				/*
+				 * Check encryption parameter.  All parents must have the same
+				 * encryption settings for a column.
+				 */
+				if ((def->encryption && !attribute->attcek) ||
+					(!def->encryption && attribute->attcek))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && attribute->attcek)
+				{
+					/*
+					 * Merging the encryption properties of two encrypted
+					 * parent columns is not yet implemented.  Right now, this
+					 * would confuse the checks of the type etc. below (we
+					 * must check the physical and the real types against each
+					 * other, respectively), which might require a larger
+					 * restructuring.  For now, just give up here.
+					 */
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("multiple inheritance of encrypted columns is not implemented")));
+				}
+
+				/*
+				 * Must have the same type, typmod, and collation.
+				 */
 				typenameTypeIdAndMod(NULL, def->typeName, &defTypeId, &deftypmod);
 				if (defTypeId != attribute->atttypid ||
 					deftypmod != attribute->atttypmod)
@@ -2641,6 +2684,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+														attribute->attusertypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -2919,6 +2968,34 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								 errdetail("%s versus %s", def->compression, newdef->compression)));
 				}
 
+				/*
+				 * Check encryption parameter.  All parents and children must
+				 * have the same encryption settings for a column.
+				 */
+				if ((def->encryption && !newdef->encryption) ||
+					(!def->encryption && newdef->encryption))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && newdef->encryption)
+				{
+					FormData_pg_attribute a, newa;
+
+					GetColumnEncryption(def->encryption, &a);
+					GetColumnEncryption(newdef->encryption, &newa);
+
+					if (a.atttypid != newa.atttypid ||
+						a.atttypmod != newa.atttypmod ||
+						a.attcek != newa.attcek)
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" has an encryption specification conflict",
+										attributeName)));
+				}
+
 				/* Mark the column as locally defined */
 				def->is_local = true;
 				/* Merge of NOT NULL constraints = OR 'em together */
@@ -6861,6 +6938,19 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attislocal = colDef->is_local;
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
+	if (colDef->encryption)
+	{
+		GetColumnEncryption(colDef->encryption, &attribute);
+		aclresult = object_aclcheck(ColumnEncKeyRelationId, attribute.attcek, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attribute.attcek, false));
+	}
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attusertypid = 0;
+		attribute.attusertypmod = -1;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12692,6 +12782,9 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
 			case OCLASS_COLLATION:
+			case OCLASS_CEK:
+			case OCLASS_CEKDATA:
+			case OCLASS_CMK:
 			case OCLASS_CONVERSION:
 			case OCLASS_LANGUAGE:
 			case OCLASS_LARGEOBJECT:
@@ -19291,3 +19384,110 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	List	   *cek = NULL;
+	Oid			cekoid;
+	bool		encdet = false;
+	int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+	foreach(lc, coldefencryption)
+	{
+		DefElem    *el = lfirst_node(DefElem, lc);
+
+		if (strcmp(el->defname, "column_encryption_key") == 0)
+			cek = defGetQualifiedName(el);
+		else if (strcmp(el->defname, "encryption_type") == 0)
+		{
+			char	   *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+			if (strcmp(val, "deterministic") == 0)
+				encdet = true;
+			else if (strcmp(val, "randomized") == 0)
+				encdet = false;
+			else
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption type: %s", val));
+		}
+		else if (strcmp(el->defname, "algorithm") == 0)
+		{
+			char	   *val = strVal(el->arg);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption algorithm: %s", val));
+		}
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized column encryption parameter: %s", el->defname));
+	}
+
+	if (!cek)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+				errmsg("column encryption key must be specified"));
+
+	cekoid = get_cek_oid(cek, false);
+
+	attr->attcek = cekoid;
+	attr->attusertypid = attr->atttypid;
+	attr->attusertypmod = attr->atttypmod;
+
+	/* override physical type */
+	if (encdet)
+		attr->atttypid = PG_ENCRYPTED_DETOID;
+	else
+		attr->atttypid = PG_ENCRYPTED_RNDOID;
+	get_typlenbyvalalign(attr->atttypid,
+						 &attr->attlen, &attr->attbyval, &attr->attalign);
+	attr->attstorage = get_typstorage(attr->atttypid);
+	attr->attcollation = InvalidOid;
+
+	attr->atttypmod = alg;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	List	   *result;
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	char	   *nspname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attr->attcek));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attr->attcek);
+
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+	nspname = get_namespace_name(form->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(form->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->atttypmod))),
+									-1));
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index bb0f5de4c2..cec2daa9c3 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -25,6 +25,7 @@
 #include "access/xlogprefetcher.h"
 #include "catalog/pg_authid.h"
 #include "common/string.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "postmaster/postmaster.h"
@@ -706,7 +707,11 @@ check_client_encoding(char **newval, void **extra, GucSource source)
 	 */
 	if (PrepareClientEncoding(encoding) < 0)
 	{
-		if (IsTransactionState())
+		if (MyProcPort->column_encryption_enabled)
+			GUC_check_errdetail("Conversion between %s and %s is not possible when column encryption is enabled.",
+								canonical_name,
+								GetDatabaseEncodingName());
+		else if (IsTransactionState())
 		{
 			/* Must be a genuine no-such-conversion problem */
 			GUC_check_errcode(ERRCODE_FEATURE_NOT_SUPPORTED);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index ff98c773f5..171dca3803 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index dc8415a693..c81bc15128 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3983,6 +3983,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(coldef->compression))
 					return true;
+				if (WALK(coldef->encryption))
+					return true;
 				if (WALK(coldef->raw_default))
 					return true;
 				if (WALK(coldef->collClause))
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a0138382a1..a6039878fa 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,6 +420,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <list>	parse_toplevel stmtmulti routine_body_stmt_list
 				OptTableElementList TableElementList OptInherit definition
+				list_of_definitions
 				OptTypedTableElementList TypedTableElementList
 				reloptions opt_reloptions
 				OptWith opt_definition func_args func_args_list
@@ -592,6 +594,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableConstraint TableLikeClause
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression column_storage opt_column_storage
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_match
@@ -690,8 +693,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
 	DOUBLE_P DROP
 
-	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
-	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EACH ELSE ENABLE_P ENCODING ENCRYPTION ENCRYPTED END_P ENUM_P ESCAPE
+	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -708,13 +711,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	JOIN
 
-	KEY
+	KEY KEYS
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
-	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+	MAPPING MASTER MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
 
 	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -1988,7 +1993,7 @@ CheckPointStmt:
 
 /*****************************************************************************
  *
- * DISCARD { ALL | TEMP | PLANS | SEQUENCES }
+ * DISCARD
  *
  *****************************************************************************/
 
@@ -2014,6 +2019,13 @@ DiscardStmt:
 					n->target = DISCARD_TEMP;
 					$$ = (Node *) n;
 				}
+			| DISCARD COLUMN ENCRYPTION KEYS
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+
+					n->target = DISCARD_COLUMN_ENCRYPTION_KEYS;
+					$$ = (Node *) n;
+				}
 			| DISCARD PLANS
 				{
 					DiscardStmt *n = makeNode(DiscardStmt);
@@ -3700,14 +3712,15 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_storage opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_encryption opt_column_storage opt_column_compression create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 
 					n->colname = $1;
 					n->typeName = $2;
-					n->storage_name = $3;
-					n->compression = $4;
+					n->encryption = $3;
+					n->storage_name = $4;
+					n->compression = $5;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3716,8 +3729,8 @@ columnDef:	ColId Typename opt_column_storage opt_column_compression create_gener
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $5;
-					SplitColQualList($6, &n->constraints, &n->collClause,
+					n->fdwoptions = $6;
+					SplitColQualList($7, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *) n;
@@ -3774,6 +3787,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4052,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6289,33 @@ DefineStmt:
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+			| CREATE COLUMN ENCRYPTION KEY any_name WITH VALUES list_of_definitions
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CEK;
+					n->defnames = $5;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name WITH definition
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = $7;
+					$$ = (Node *) n;
+				}
 		;
 
 definition: '(' def_list ')'						{ $$ = $2; }
@@ -6289,6 +6335,10 @@ def_elem:	ColLabel '=' def_arg
 				}
 		;
 
+list_of_definitions: definition						{ $$ = list_make1($1); }
+			| list_of_definitions ',' definition	{ $$ = lappend($1, $3); }
+		;
+
 /* Note: any simple identifier will be returned as a type name! */
 def_arg:	func_type						{ $$ = (Node *) $1; }
 			| reserved_keyword				{ $$ = (Node *) makeString(pstrdup($1)); }
@@ -6800,6 +6850,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -7611,6 +7663,24 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| COLUMN ENCRYPTION KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CEK;
+					n->objs = $4;
+					$$ = n;
+				}
+			| COLUMN MASTER KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CMK;
+					n->objs = $4;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9140,6 +9210,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9817,6 +9907,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10148,6 +10258,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11304,6 +11432,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_name ADD_P VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = false;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN ENCRYPTION KEY any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16965,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16840,6 +17015,7 @@ unreserved_keyword:
 			| INVOKER
 			| ISOLATION
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -16854,6 +17030,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17514,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17404,6 +17582,7 @@ bare_label_keyword:
 			| ISOLATION
 			| JOIN
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -17425,6 +17604,7 @@ bare_label_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index 2240284f21..a8a1855aa0 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,159 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			Expr	   *expr = te->expr;
+
+			/*
+			 * If it's a RelabelType, look inside.  (For encrypted columns,
+			 * this would typically be a typmod adjustment.)
+			 */
+			if (IsA(expr, RelabelType))
+				expr = castNode(RelabelType, expr)->arg;
+
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(expr, Param))
+			{
+				Param	   *p = (Param *) expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(expr, Var))
+			{
+				Var		   *v = (Var *) expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell   *lc;
+
+					foreach(lc, srcrte->values_lists)
+					{
+						List	   *values_list = lfirst_node(List, lc);
+						Expr	   *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, RelabelType))
+							value = castNode(RelabelType, value)->arg;
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach(lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f9218f48aa..8d902292cd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1035,8 +1035,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+												attribute->attusertypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2552327d90..79ec9c4d1f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2210,12 +2210,27 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 									valptr),
 							 errhint("Valid values are: \"false\", 0, \"true\", 1, \"database\".")));
 			}
+			else if (strcmp(nameptr, "_pq_.column_encryption") == 0)
+			{
+				/*
+				 * Right now, the only accepted value is "1".  This gives room
+				 * to expand this into a version number, for example.
+				 */
+				if (strcmp(valptr, "1") == 0)
+					port->column_encryption_enabled = true;
+				else
+					ereport(FATAL,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for parameter \"%s\": \"%s\"",
+									"column_encryption",
+									valptr),
+							 errhint("Valid values are: 1.")));
+			}
 			else if (strncmp(nameptr, "_pq_.", 5) == 0)
 			{
 				/*
 				 * Any option beginning with _pq_. is reserved for use as a
-				 * protocol-level option, but at present no such options are
-				 * defined.
+				 * protocol-level option.
 				 */
 				unrecognized_protocol_options =
 					lappend(unrecognized_protocol_options, pstrdup(nameptr));
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index cab709b07b..b577557384 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -71,6 +72,7 @@
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
 
@@ -1815,6 +1817,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (type_is_encrypted(ptype))
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter $%d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2560,6 +2572,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2618,11 +2632,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	for (int i = 0; i < psrc->num_params; i++)
 	{
 		Oid			ptype = psrc->param_types[i];
+		Oid			pcekid = InvalidOid;
+		int			pcekalg = 0;
+		int16		pflags = 0;
+
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(ptype))
+		{
+			Oid			porigtbl = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("parameter $%d corresponds to an encrypted column, but an underlying table and column could not be determined", i + 1)));
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(porigtbl), Int16GetDatum(porigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", porigcol, porigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			ptype = orig_att->attusertypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x0001;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&row_description_buf, (int) pcekid);
+			pq_sendint32(&row_description_buf, pcekalg);
+			pq_sendint16(&row_description_buf, pflags);
+		}
 	}
 	pq_endmessage_reuse(&row_description_buf);
 
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index c7d9d96b45..180f86cb79 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -30,6 +30,7 @@
 #include "commands/alter.h"
 #include "commands/async.h"
 #include "commands/cluster.h"
+#include "commands/colenccmds.h"
 #include "commands/collationcmds.h"
 #include "commands/comment.h"
 #include "commands/conversioncmds.h"
@@ -137,6 +138,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,14 @@ ProcessUtilitySlow(ParseState *pstate,
 															stmt->definition,
 															&secondaryObject);
 							break;
+						case OBJECT_CEK:
+							Assert(stmt->args == NIL);
+							address = CreateCEK(pstate, stmt);
+							break;
+						case OBJECT_CMK:
+							Assert(stmt->args == NIL);
+							address = CreateCMK(pstate, stmt);
+							break;
 						case OBJECT_COLLATION:
 							Assert(stmt->args == NIL);
 							address = DefineCollation(pstate,
@@ -1903,6 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,12 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_COLUMN:
 			tag = CMDTAG_ALTER_TABLE;
 			break;
+		case OBJECT_CEK:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+		case OBJECT_CMK:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
 		case OBJECT_CONVERSION:
 			tag = CMDTAG_ALTER_CONVERSION;
 			break;
@@ -2640,6 +2665,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_DROP_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_DROP_COLUMN_MASTER_KEY;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2760,6 +2791,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_COLLATION:
 					tag = CMDTAG_CREATE_COLLATION;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_CREATE_COLUMN_MASTER_KEY;
+					break;
 				case OBJECT_ACCESS_METHOD:
 					tag = CMDTAG_CREATE_ACCESS_METHOD;
 					break;
@@ -2917,6 +2954,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_ALL:
 					tag = CMDTAG_DISCARD_ALL;
 					break;
+				case DISCARD_COLUMN_ENCRYPTION_KEYS:
+					tag = CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS;
+					break;
 				case DISCARD_PLANS:
 					tag = CMDTAG_DISCARD_PLANS;
 					break;
@@ -3063,6 +3103,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3736,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 8f7522d103..6aeb06f8fd 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -22,6 +22,8 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_foreign_data_wrapper.h"
 #include "catalog/pg_foreign_server.h"
@@ -101,6 +103,10 @@ static AclMode convert_table_priv_string(text *priv_type_text);
 static AclMode convert_sequence_priv_string(text *priv_type_text);
 static AttrNumber convert_column_name(Oid tableoid, text *column);
 static AclMode convert_column_priv_string(text *priv_type_text);
+static Oid	convert_column_encryption_key_name(text *cekname);
+static AclMode convert_column_encryption_key_priv_string(text *priv_type_text);
+static Oid	convert_column_master_key_name(text *cmkname);
+static AclMode convert_column_master_key_priv_string(text *priv_type_text);
 static Oid	convert_database_name(text *databasename);
 static AclMode convert_database_priv_string(text *priv_type_text);
 static Oid	convert_foreign_data_wrapper_name(text *fdwname);
@@ -800,6 +806,14 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			/* for backwards compatibility, grant some rights by default */
 			world_default = ACL_CREATE_TEMP | ACL_CONNECT;
@@ -911,6 +925,12 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 's':
 			objtype = OBJECT_SEQUENCE;
 			break;
+		case 'Y':
+			objtype = OBJECT_CEK;
+			break;
+		case 'y':
+			objtype = OBJECT_CMK;
+			break;
 		case 'd':
 			objtype = OBJECT_DATABASE;
 			break;
@@ -2915,6 +2935,384 @@ convert_column_priv_string(text *priv_type_text)
 }
 
 
+/*
+ * has_column_encryption_key_privilege variants
+ *		These are all named "has_column_encryption_key_privilege" at the SQL level.
+ *		They take various combinations of column encryption key name,
+ *		cek OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_encryption_key_privilege_name_name
+ *		Check user privileges on a column encryption key given
+ *		name username, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name
+ *		Check user privileges on a column encryption key given
+ *		text cekname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cekname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name_id
+ *		Check user privileges on a column encryption key given
+ *		name usename, column encryption key oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id
+ *		Check user privileges on a column encryption key given
+ *		column encryption key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cekid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_name
+ *		Check user privileges on a column encryption key given
+ *		roleid, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_id
+ *		Check user privileges on a column encryption key given
+ *		roleid, cek oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_encryption_key_privilege family.
+ */
+
+/*
+ * Given a CEK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_encryption_key_name(text *cekname)
+{
+	return get_cek_oid(textToQualifiedNameList(cekname), false);
+}
+
+/*
+ * convert_column_encryption_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_encryption_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_encryption_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_encryption_key_priv_map);
+}
+
+
+/*
+ * has_column_master_key_privilege variants
+ *		These are all named "has_column_master_key_privilege" at the SQL level.
+ *		They take various combinations of column master key name,
+ *		cmk OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_master_key_privilege_name_name
+ *		Check user privileges on a column master key given
+ *		name username, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name
+ *		Check user privileges on a column master key given
+ *		text cmkname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cmkname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name_id
+ *		Check user privileges on a column master key given
+ *		name usename, column master key oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id
+ *		Check user privileges on a column master key given
+ *		column master key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cmkid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_name
+ *		Check user privileges on a column master key given
+ *		roleid, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_id
+ *		Check user privileges on a column master key given
+ *		roleid, cmk oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_master_key_privilege family.
+ */
+
+/*
+ * Given a CMK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_master_key_name(text *cmkname)
+{
+	return get_cmk_oid(textToQualifiedNameList(cmkname), false);
+}
+
+/*
+ * convert_column_master_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_master_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_master_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_master_key_priv_map);
+}
+
+
 /*
  * has_database_privilege variants
  *		These are all named "has_database_privilege" at the SQL level.
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 5778e3f0ef..dd21fcf59e 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -679,6 +679,113 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	Node	   *escontext = fcinfo->context;
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c07382051d..7ad159110f 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -24,7 +24,10 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_namespace.h"
@@ -2658,6 +2661,25 @@ type_is_multirange(Oid typid)
 	return (get_typtype(typid) == TYPTYPE_MULTIRANGE);
 }
 
+bool
+type_is_encrypted(Oid typid)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+		bool		result;
+
+		result = (typtup->typcategory == TYPCATEGORY_ENCRYPTED);
+		ReleaseSysCache(tp);
+		return result;
+	}
+	else
+		return false;
+}
+
 /*
  * get_type_category_preferred
  *
@@ -3683,3 +3705,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+char *
+get_cek_name(Oid cekid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cekname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+		return NULL;
+	}
+
+	cekname = pstrdup(NameStr(((Form_pg_colenckey) GETSTRUCT(tup))->cekname));
+
+	ReleaseSysCache(tup);
+
+	return cekname;
+}
+
+Oid
+get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok)
+{
+	Oid			cekdataid;
+
+	cekdataid = GetSysCacheOid2(CEKDATACEKCMK, Anum_pg_colenckeydata_oid,
+								ObjectIdGetDatum(cekid),
+								ObjectIdGetDatum(cmkid));
+	if (!OidIsValid(cekdataid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" has no data for master key \"%s\"",
+						get_cek_name(cekid, false), get_cmk_name(cmkid, false))));
+
+	return cekdataid;
+}
+
+char *
+get_cmk_name(Oid cmkid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cmkname;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+		return NULL;
+	}
+
+	cmkname = pstrdup(NameStr(((Form_pg_colmasterkey) GETSTRUCT(tup))->cmkname));
+
+	ReleaseSysCache(tup);
+
+	return cmkname;
+}
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 77c2ba3f8f..d40af13efe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 94abede512..eb432260da 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -219,6 +222,32 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAMENSP] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameNspIndexId,
+		KEY(Anum_pg_colenckey_cekname,
+			Anum_pg_colenckey_ceknamespace),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +262,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAMENSP] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameNspIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname,
+			Anum_pg_colmasterkey_cmknamespace),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 033647011b..4b6be68f27 100644
--- a/src/backend/utils/mb/mbutils.c
+++ b/src/backend/utils/mb/mbutils.c
@@ -36,7 +36,9 @@
 
 #include "access/xact.h"
 #include "catalog/namespace.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
@@ -130,6 +132,12 @@ PrepareClientEncoding(int encoding)
 		encoding == PG_SQL_ASCII)
 		return 0;
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	if (IsTransactionState())
 	{
 		/*
@@ -237,6 +245,12 @@ SetClientEncoding(int encoding)
 		return 0;
 	}
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	/*
 	 * Search the cache for the entry previously prepared by
 	 * PrepareClientEncoding; if there isn't one, we lose.  While at it,
@@ -297,7 +311,9 @@ InitializeClientEncoding(void)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("conversion between %s and %s is not supported",
 						pg_enc2name_tbl[pending_client_encoding].name,
-						GetDatabaseEncodingName())));
+						GetDatabaseEncodingName()),
+				 (MyProcPort->column_encryption_enabled) ?
+				 errdetail("Encoding conversion is not possible when column encryption is enabled.") : 0));
 	}
 
 	/*
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index a43f2e5553..e67462c81a 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -201,6 +203,12 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading user-defined collations");
 	(void) getCollations(fout, &numCollations);
 
+	pg_log_info("reading column master keys");
+	getColumnMasterKeys(fout);
+
+	pg_log_info("reading column encryption keys");
+	getColumnEncryptionKeys(fout);
+
 	pg_log_info("reading user-defined conversions");
 	getConversions(fout, &numConversions);
 
@@ -859,6 +867,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 9753a6d868..0a5178bcf1 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -484,6 +484,10 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('U', "USAGE");
 	}
+	else if (strcmp(type, "COLUMN ENCRYPTION KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "COLUMN MASTER KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "DATABASE") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,7 @@ typedef struct _connParams
 	char	   *pghost;
 	char	   *username;
 	trivalue	promptPassword;
+	int			column_encryption;
 	/* If not NULL, this overrides the dbname obtained from command line */
 	/* (but *only* the DB name, not anything else in the connstring) */
 	char	   *override_dbname;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 61ebb8fe85..bc303550fd 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3396,6 +3396,8 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 
 	/* objects that don't require special decoration */
 	if (strcmp(type, "COLLATION") == 0 ||
+		strcmp(type, "COLUMN ENCRYPTION KEY") == 0 ||
+		strcmp(type, "COLUMN MASTER KEY") == 0 ||
 		strcmp(type, "CONVERSION") == 0 ||
 		strcmp(type, "DOMAIN") == 0 ||
 		strcmp(type, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 100644
--- a/src/bin/pg_dump/pg_backup_db.c
+++ b/src/bin/pg_dump/pg_backup_db.c
@@ -133,8 +133,8 @@ ConnectDatabase(Archive *AHX,
 	 */
 	do
 	{
-		const char *keywords[8];
-		const char *values[8];
+		const char *keywords[9];
+		const char *values[9];
 		int			i = 0;
 
 		/*
@@ -159,6 +159,11 @@ ConnectDatabase(Archive *AHX,
 		}
 		keywords[i] = "fallback_application_name";
 		values[i++] = progname;
+		if (cparams->column_encryption)
+		{
+			keywords[i] = "column_encryption";
+			values[i++] = "1";
+		}
 		keywords[i] = NULL;
 		values[i++] = NULL;
 		Assert(i <= lengthof(keywords));
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 24ba936332..8c61fcab13 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,8 @@ static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
 static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
 static void dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo);
 static void dumpCollation(Archive *fout, const CollInfo *collinfo);
+static void dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo);
+static void dumpColumnMasterKey(Archive *fout, const CmkInfo *cekinfo);
 static void dumpConversion(Archive *fout, const ConvInfo *convinfo);
 static void dumpRule(Archive *fout, const RuleInfo *rinfo);
 static void dumpAgg(Archive *fout, const AggInfo *agginfo);
@@ -393,6 +396,7 @@ main(int argc, char **argv)
 		{"attribute-inserts", no_argument, &dopt.column_inserts, 1},
 		{"binary-upgrade", no_argument, &dopt.binary_upgrade, 1},
 		{"column-inserts", no_argument, &dopt.column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &dopt.cparams.column_encryption, 1},
 		{"disable-dollar-quoting", no_argument, &dopt.disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &dopt.disable_triggers, 1},
 		{"enable-row-security", no_argument, &dopt.enable_row_security, 1},
@@ -685,6 +689,9 @@ main(int argc, char **argv)
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
 	 */
+	if (dopt.cparams.column_encryption && dopt.dump_inserts == 0)
+		pg_fatal("option --decrypt-encrypted-columns requires option --inserts, --rows-per-insert, or --column-inserts");
+
 	if (dopt.do_nothing && dopt.dump_inserts == 0)
 		pg_fatal("option --on-conflict-do-nothing requires option --inserts, --rows-per-insert, or --column-inserts");
 
@@ -1056,6 +1063,7 @@ help(const char *progname)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security (dump only content user has\n"
@@ -5571,6 +5579,164 @@ getCollations(Archive *fout, int *numCollations)
 	return collinfo;
 }
 
+/*
+ * getColumnEncryptionKeys
+ *	  get information about column encryption keys
+ */
+void
+getColumnEncryptionKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CekInfo	   *cekinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cekname;
+	int			i_ceknamespace;
+	int			i_cekowner;
+	int			i_cekacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, cek.cekowner, cek.cekacl, acldefault('Y', cek.cekowner) AS acldefault\n"
+					  "FROM pg_colenckey cek");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cekname = PQfnumber(res, "cekname");
+	i_ceknamespace = PQfnumber(res, "ceknamespace");
+	i_cekowner = PQfnumber(res, "cekowner");
+	i_cekacl = PQfnumber(res, "cekacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cekinfo = pg_malloc(ntups * sizeof(CekInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		PGresult	   *res2;
+		int				ntups2;
+
+		cekinfo[i].dobj.objType = DO_CEK;
+		cekinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cekinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cekinfo[i].dobj);
+		cekinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cekname));
+		cekinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cekacl));
+		cekinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cekinfo[i].dacl.privtype = 0;
+		cekinfo[i].dacl.initprivs = NULL;
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, ckdcmkalg, ckdencval\n"
+						  "FROM pg_catalog.pg_colenckeydata\n"
+						  "WHERE ckdcekid = %u", cekinfo[i].dobj.catId.oid);
+		res2 = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		ntups2 = PQntuples(res2);
+		cekinfo[i].numdata = ntups2;
+		cekinfo[i].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			cekinfo[i].cekcmkalgs[j] = atoi(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkalg")));
+			cekinfo[i].cekencvals[j] = pg_strdup(PQgetvalue(res2, j, PQfnumber(res2, "ckdencval")));
+		}
+		PQclear(res2);
+
+		selectDumpableObject(&(cekinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cekacl))
+			cekinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * getColumnMasterKeys
+ *	  get information about column master keys
+ */
+void
+getColumnMasterKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CmkInfo	   *cmkinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cmkname;
+	int			i_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+	int			i_cmkacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, cmk.cmkowner, cmk.cmkrealm, cmk.cmkacl, acldefault('y', cmk.cmkowner) AS acldefault\n"
+					  "FROM pg_colmasterkey cmk");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cmkname = PQfnumber(res, "cmkname");
+	i_cmknamespace = PQfnumber(res, "cmknamespace");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+	i_cmkacl = PQfnumber(res, "cmkacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cmkinfo = pg_malloc(ntups * sizeof(CmkInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		cmkinfo[i].dobj.objType = DO_CMK;
+		cmkinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cmkinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cmkinfo[i].dobj);
+		cmkinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cmkname));
+		cmkinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		cmkinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cmkacl));
+		cmkinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cmkinfo[i].dacl.privtype = 0;
+		cmkinfo[i].dacl.initprivs = NULL;
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cmkacl))
+			cmkinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8186,6 +8352,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8245,8 +8414,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
+	appendPQExpBuffer(q, "SELECT\n"
 						 "a.attrelid,\n"
 						 "a.attnum,\n"
 						 "a.attname,\n"
@@ -8259,7 +8427,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attlen,\n"
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
+						 "pg_catalog.format_type(%s) AS atttypname,\n"
 						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
@@ -8268,7 +8436,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "' ' || pg_catalog.quote_literal(option_value) "
 						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
 						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+						 "), E',\n    ') AS attfdwoptions,\n",
+						 fout->remoteVersion >= 160000 ?
+						 "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END, CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END" :
+						 "a.atttypid, a.atttypmod");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -8294,10 +8465,23 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	if (fout->remoteVersion >= 120000)
 		appendPQExpBufferStr(q,
-							 "a.attgenerated\n");
+							 "a.attgenerated,\n");
+	else
+		appendPQExpBufferStr(q,
+							 "'' AS attgenerated,\n");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBuffer(q,
+						  "a.attcek,\n"
+						  "CASE a.atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "CASE WHEN a.atttypid IN (%u, %u) THEN a.atttypmod END AS attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID,
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcek,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
@@ -8322,6 +8506,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	i_attencdet = PQfnumber(res, "attencdet");
+	i_attencalg = PQfnumber(res, "attencalg");
 	i_attisdropped = PQfnumber(res, "attisdropped");
 	i_attlen = PQfnumber(res, "attlen");
 	i_attalign = PQfnumber(res, "attalign");
@@ -8382,6 +8569,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		tbinfo->typstorage = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attidentity = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attgenerated = (char *) pg_malloc(numatts * sizeof(char));
+		tbinfo->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		tbinfo->attencdet = (bool *) pg_malloc(numatts * sizeof(bool));
+		tbinfo->attencalg = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char));
@@ -8409,6 +8599,22 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->attidentity[j] = *(PQgetvalue(res, r, i_attidentity));
 			tbinfo->attgenerated[j] = *(PQgetvalue(res, r, i_attgenerated));
 			tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
+			if (!PQgetisnull(res, r, i_attcek))
+			{
+				Oid		attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[j] = NULL;
+			if (!PQgetisnull(res, r, i_attencdet))
+				tbinfo->attencdet[j] = (PQgetvalue(res, r, i_attencdet)[0] == 't');
+			else
+				tbinfo->attencdet[j] = 0;
+			if (!PQgetisnull(res, r, i_attencalg))
+				tbinfo->attencalg[j] = atoi(PQgetvalue(res, r, i_attencalg));
+			else
+				tbinfo->attencalg[j] = 0;
 			tbinfo->attisdropped[j] = (PQgetvalue(res, r, i_attisdropped)[0] == 't');
 			tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
 			tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
@@ -9927,6 +10133,12 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_OPFAMILY:
 			dumpOpfamily(fout, (const OpfamilyInfo *) dobj);
 			break;
+		case DO_CEK:
+			dumpColumnEncryptionKey(fout, (const CekInfo *) dobj);
+			break;
+		case DO_CMK:
+			dumpColumnMasterKey(fout, (const CmkInfo *) dobj);
+			break;
 		case DO_COLLATION:
 			dumpCollation(fout, (const CollInfo *) dobj);
 			break;
@@ -13320,6 +13532,141 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * dumpColumnEncryptionKey
+ *	  dump the definition of the given column encryption key
+ */
+static void
+dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcekname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcekname = pg_strdup(fmtId(cekinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN ENCRYPTION KEY %s;\n",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_name(cekinfo->cekcmkalgs[i]));
+		appendPQExpBuffer(query, "encrypted_value = ");
+		appendStringLiteralAH(query, cekinfo->cekencvals[i], fout);
+
+		appendPQExpBuffer(query, ")");
+		if (i < cekinfo->numdata - 1)
+			appendPQExpBuffer(query, ", ");
+	}
+
+	appendPQExpBufferStr(query, ";\n");
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cekinfo->dobj.catId, cekinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cekinfo->dobj.name,
+								  .namespace = cekinfo->dobj.namespace->dobj.name,
+								  .owner = cekinfo->rolname,
+								  .description = "COLUMN ENCRYPTION KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cekinfo->dobj.dumpId, InvalidDumpId, "COLUMN ENCRYPTION KEY",
+				qcekname, NULL, cekinfo->dobj.namespace->dobj.name,
+				cekinfo->rolname, &cekinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcekname);
+}
+
+/*
+ * dumpColumnMasterKey
+ *	  dump the definition of the given column master key
+ */
+static void
+dumpColumnMasterKey(Archive *fout, const CmkInfo *cmkinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcmkname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcmkname = pg_strdup(fmtId(cmkinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN MASTER KEY %s;\n",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "realm = ");
+	appendStringLiteralAH(query, cmkinfo->cmkrealm, fout);
+
+	appendPQExpBufferStr(query, ");\n");
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cmkinfo->dobj.catId, cmkinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cmkinfo->dobj.name,
+								  .namespace = cmkinfo->dobj.namespace->dobj.name,
+								  .owner = cmkinfo->rolname,
+								  .description = "COLUMN MASTER KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN MASTER KEY", qcmkname,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cmkinfo->dobj.dumpId, InvalidDumpId, "COLUMN MASTER KEY",
+				qcmkname, NULL, cmkinfo->dobj.namespace->dobj.name,
+				cmkinfo->rolname, &cmkinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15403,6 +15750,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[j]));
+						/*
+						 * To reduce output size, we don't print the default
+						 * of encryption_type, but we do print the default of
+						 * algorithm, since we might want to change to a new
+						 * default algorithm sometime in the future.
+						 */
+						if (tbinfo->attencdet[j])
+							appendPQExpBuffer(q, "encryption_type = deterministic, ");
+						appendPQExpBuffer(q, "algorithm = '%s')",
+										  get_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -17971,6 +18334,8 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_ACCESS_METHOD:
 			case DO_OPCLASS:
 			case DO_OPFAMILY:
+			case DO_CEK:
+			case DO_CMK:
 			case DO_COLLATION:
 			case DO_CONVERSION:
 			case DO_TABLE:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index cdca0b993d..d4a2e595d0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -47,6 +47,8 @@ typedef enum
 	DO_ACCESS_METHOD,
 	DO_OPCLASS,
 	DO_OPFAMILY,
+	DO_CEK,
+	DO_CMK,
 	DO_COLLATION,
 	DO_CONVERSION,
 	DO_TABLE,
@@ -332,6 +334,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	struct _CekInfo **attcek;
+	int		   *attencalg;
+	bool	   *attencdet;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
 	char	   *attalign;		/* attribute align, used by binary_upgrade */
 	bool	   *attislocal;		/* true if attr has local definition */
@@ -663,6 +668,32 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	struct _CmkInfo **cekcmks;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -683,6 +714,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -710,6 +743,8 @@ extern AccessMethodInfo *getAccessMethods(Archive *fout, int *numAccessMethods);
 extern OpclassInfo *getOpclasses(Archive *fout, int *numOpclasses);
 extern OpfamilyInfo *getOpfamilies(Archive *fout, int *numOpfamilies);
 extern CollInfo *getCollations(Archive *fout, int *numCollations);
+extern void getColumnEncryptionKeys(Archive *fout);
+extern void getColumnMasterKeys(Archive *fout);
 extern ConvInfo *getConversions(Archive *fout, int *numConversions);
 extern TableInfo *getTables(Archive *fout, int *numTables);
 extern void getOwnedSeqs(Archive *fout, TableInfo tblinfo[], int numTables);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 8266c117a3..d3dacd39da 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -69,6 +69,8 @@ enum dbObjectTypePriorities
 	PRIO_TSTEMPLATE,
 	PRIO_TSDICT,
 	PRIO_TSCONFIG,
+	PRIO_CMK,
+	PRIO_CEK,
 	PRIO_FDW,
 	PRIO_FOREIGN_SERVER,
 	PRIO_TABLE,
@@ -111,6 +113,8 @@ static const int dbObjectTypePriority[] =
 	PRIO_ACCESS_METHOD,			/* DO_ACCESS_METHOD */
 	PRIO_OPFAMILY,				/* DO_OPCLASS */
 	PRIO_OPFAMILY,				/* DO_OPFAMILY */
+	PRIO_CEK,					/* DO_CEK */
+	PRIO_CMK,					/* DO_CMK */
 	PRIO_COLLATION,				/* DO_COLLATION */
 	PRIO_CONVERSION,			/* DO_CONVERSION */
 	PRIO_TABLE,					/* DO_TABLE */
@@ -1322,6 +1326,16 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "OPERATOR FAMILY %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_CEK:
+			snprintf(buf, bufsize,
+					 "COLUMN ENCRYPTION KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
+		case DO_CMK:
+			snprintf(buf, bufsize,
+					 "COLUMN MASTER KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_COLLATION:
 			snprintf(buf, bufsize,
 					 "COLLATION %s  (ID %d OID %u)",
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index cd421c5944..6530fb81a2 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -93,6 +93,7 @@ static bool dosync = true;
 
 static int	binary_upgrade = 0;
 static int	column_inserts = 0;
+static int	decrypt_encrypted_columns = 0;
 static int	disable_dollar_quoting = 0;
 static int	disable_triggers = 0;
 static int	if_exists = 0;
@@ -154,6 +155,7 @@ main(int argc, char *argv[])
 		{"attribute-inserts", no_argument, &column_inserts, 1},
 		{"binary-upgrade", no_argument, &binary_upgrade, 1},
 		{"column-inserts", no_argument, &column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &decrypt_encrypted_columns, 1},
 		{"disable-dollar-quoting", no_argument, &disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &disable_triggers, 1},
 		{"exclude-database", required_argument, NULL, 6},
@@ -424,6 +426,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --binary-upgrade");
 	if (column_inserts)
 		appendPQExpBufferStr(pgdumpopts, " --column-inserts");
+	if (decrypt_encrypted_columns)
+		appendPQExpBufferStr(pgdumpopts, " --decrypt-encrypted-columns");
 	if (disable_dollar_quoting)
 		appendPQExpBufferStr(pgdumpopts, " --disable-dollar-quoting");
 	if (disable_triggers)
@@ -649,6 +653,7 @@ help(void)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --exclude-database=PATTERN   exclude databases whose name matches PATTERN\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 72b19ee6cd..837d515639 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -719,6 +719,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
 	'ALTER FOREIGN DATA WRAPPER dummy OWNER TO' => {
 		regexp => qr/^ALTER FOREIGN DATA WRAPPER dummy OWNER TO .+;/m,
 		like   => { %full_runs, section_pre_data => 1, },
@@ -1319,6 +1331,26 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1737,6 +1769,26 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3570,6 +3622,26 @@
 		unlike => { no_privs => 1, },
 	},
 
+	'GRANT USAGE ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
+	'GRANT USAGE ON COLUMN MASTER KEY cmk1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
 	'GRANT USAGE ON FOREIGN DATA WRAPPER dummy' => {
 		create_order => 85,
 		create_sql   => 'GRANT USAGE ON FOREIGN DATA WRAPPER dummy
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..0d6a46d24b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c8a0bb7b3a..04d437f836 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1530,7 +1530,7 @@ describeOneTableDetails(const char *schemaname,
 	bool		printTableInitialized = false;
 	int			i;
 	char	   *view_def = NULL;
-	char	   *headers[12];
+	char	   *headers[13];
 	PQExpBufferData title;
 	PQExpBufferData tmpbuf;
 	int			cols;
@@ -1546,6 +1546,7 @@ describeOneTableDetails(const char *schemaname,
 				fdwopts_col = -1,
 				attstorage_col = -1,
 				attcompression_col = -1,
+				attcekname_col = -1,
 				attstattarget_col = -1,
 				attdescr_col = -1;
 	int			numrows;
@@ -1568,6 +1569,8 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attusertypid;
+	const char *attusertypmod;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1844,7 +1847,17 @@ describeOneTableDetails(const char *schemaname,
 	cols = 0;
 	printfPQExpBuffer(&buf, "SELECT a.attname");
 	attname_col = cols++;
-	appendPQExpBufferStr(&buf, ",\n  pg_catalog.format_type(a.atttypid, a.atttypmod)");
+	if (pset.sversion >= 160000)
+	{
+		attusertypid = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END";
+		attusertypmod = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END";
+	}
+	else
+	{
+		attusertypid = "a.atttypid";
+		attusertypmod = "a.atttypmod";
+	}
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, %s)", attusertypid, attusertypmod);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1857,7 +1870,8 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		attrdef_col = cols++;
 		attnotnull_col = cols++;
-		appendPQExpBufferStr(&buf, ",\n  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
+		appendPQExpBufferStr(&buf, ",\n"
+							 "  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
 							 "   WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) AS attcollation");
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
@@ -1909,6 +1923,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			appendPQExpBufferStr(&buf, ",\n  (SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek) AS attcekname");
+			attcekname_col = cols++;
+		}
+
 		/* stats target, if relevant to relkind */
 		if (tableinfo.relkind == RELKIND_RELATION ||
 			tableinfo.relkind == RELKIND_INDEX ||
@@ -2032,6 +2058,8 @@ describeOneTableDetails(const char *schemaname,
 		headers[cols++] = gettext_noop("Storage");
 	if (attcompression_col >= 0)
 		headers[cols++] = gettext_noop("Compression");
+	if (attcekname_col >= 0)
+		headers[cols++] = gettext_noop("Encryption");
 	if (attstattarget_col >= 0)
 		headers[cols++] = gettext_noop("Stats target");
 	if (attdescr_col >= 0)
@@ -2124,6 +2152,17 @@ describeOneTableDetails(const char *schemaname,
 							  false, false);
 		}
 
+		/* Column encryption */
+		if (attcekname_col >= 0)
+		{
+			if (!PQgetisnull(res, i, attcekname_col))
+				printTableAddCell(&cont, PQgetvalue(res, i, attcekname_col),
+								  false, false);
+			else
+				printTableAddCell(&cont, "",
+								  false, false);
+		}
+
 		/* Statistics target, if the relkind supports this feature */
 		if (attstattarget_col >= 0)
 			printTableAddCell(&cont, PQgetvalue(res, i, attstattarget_col),
@@ -4477,6 +4516,152 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cekacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cmkacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cmk.oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 554fe86725..1cf8f72176 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1729966959 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,8 @@ helpVariables(unsigned short int pager)
 		  "    true if last query failed, else false\n");
 	HELP0("  FETCH_COUNT\n"
 		  "    the number of result rows to fetch and display at a time (0 = unlimited)\n");
+	HELP0("  HIDE_COLUMN_ENCRYPTION\n"
+		  "    if set, column encryption details are not displayed\n");
 	HELP0("  HIDE_TABLEAM\n"
 		  "    if set, table access methods are not displayed\n");
 	HELP0("  HIDE_TOAST_COMPRESSION\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 73d4b393bc..010bc5a6d5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,7 @@ typedef struct _psqlSettings
 	bool		quiet;
 	bool		singleline;
 	bool		singlestep;
+	bool		hide_column_encryption;
 	bool		hide_compression;
 	bool		hide_tableam;
 	int			fetch_count;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 5a28b6f713..6736505c3a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -1188,6 +1188,13 @@ hide_compression_hook(const char *newval)
 							 &pset.hide_compression);
 }
 
+static bool
+hide_column_encryption_hook(const char *newval)
+{
+	return ParseVariableBool(newval, "HIDE_COLUMN_ENCRYPTION",
+							 &pset.hide_column_encryption);
+}
+
 static bool
 hide_tableam_hook(const char *newval)
 {
@@ -1259,6 +1266,9 @@ EstablishVariableSpace(void)
 	SetVariableHooks(pset.vars, "SHOW_CONTEXT",
 					 show_context_substitute_hook,
 					 show_context_hook);
+	SetVariableHooks(pset.vars, "HIDE_COLUMN_ENCRYPTION",
+					 bool_substitute_hook,
+					 hide_column_encryption_hook);
 	SetVariableHooks(pset.vars, "HIDE_TOAST_COMPRESSION",
 					 bool_substitute_hook,
 					 hide_compression_hook);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5e1882eaea..0642449fa0 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -933,6 +933,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1223,6 +1237,8 @@ static const pgsql_thing_t words_after_create[] = {
 	{"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", NULL, NULL, &Query_for_list_of_collations},
+	{"COLUMN ENCRYPTION KEY", NULL, NULL, NULL},
+	{"COLUMN MASTER KEY KEY", NULL, NULL, NULL},
 
 	/*
 	 * CREATE CONSTRAINT TRIGGER is not supported here because it is designed
@@ -1705,7 +1721,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1952,6 +1968,22 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "COLLATION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "REFRESH VERSION", "RENAME TO", "SET SCHEMA");
 
+	/* ALTER/DROP COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "ENCRYPTION", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_ceks);
+
+	/* ALTER COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("ADD VALUE (", "DROP VALUE (", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2894,6 +2926,26 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("true", "false");
 	}
 
+	/* CREATE/ALTER/DROP COLUMN ... KEY */
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN"))
+		COMPLETE_WITH("ENCRYPTION", "MASTER");
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN", "ENCRYPTION|MASTER"))
+		COMPLETE_WITH("KEY");
+
+	/* CREATE COLUMN ENCRYPTION KEY */
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("VALUES");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH", "VALUES"))
+		COMPLETE_WITH("(");
+
+	/* CREATE COLUMN MASTER KEY */
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("(");
+
 	/* CREATE DATABASE */
 	else if (Matches("CREATE", "DATABASE", MatchAny))
 		COMPLETE_WITH("OWNER", "TEMPLATE", "ENCODING", "TABLESPACE",
@@ -3605,7 +3657,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 	else if (Matches("DISCARD"))
-		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP");
+		COMPLETE_WITH("ALL", "COLUMN ENCRYPTION KEYS", "PLANS", "SEQUENCES", "TEMP");
 
 /* DO */
 	else if (Matches("DO"))
@@ -3619,6 +3671,7 @@ psql_completion(const char *text, int start, int end)
 			 Matches("DROP", "ACCESS", "METHOD", MatchAny) ||
 			 (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
 			  ends_with(prev_wd, ')')) ||
+			 Matches("DROP", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
 			 Matches("DROP", "EVENT", "TRIGGER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "TABLE", MatchAny) ||
@@ -3931,6 +3984,8 @@ psql_completion(const char *text, int start, int end)
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"COLUMN ENCRYPTION KEY",
+											"COLUMN MASTER KEY",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4046,6 +4101,16 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+	/* Complete "GRANT/REVOKE * ON COLUMN ENCRYPTION|MASTER KEY *" with TO/FROM */
+	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
+			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny))
+	{
+		if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny, MatchAny))
+			COMPLETE_WITH("TO");
+		else
+			COMPLETE_WITH("FROM");
+	}
+
 	/* Complete "GRANT/REVOKE * ON FOREIGN DATA WRAPPER *" with TO/FROM */
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny))
diff --git a/src/common/Makefile b/src/common/Makefile
index 113029bf7b..73dce1150e 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			return "AEAD_AES_128_CBC_HMAC_SHA_256";
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			return "AEAD_AES_192_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			return "AEAD_AES_256_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			return "AEAD_AES_256_CBC_HMAC_SHA_512";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index 41bd58ebdf..3695d3285b 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -4,6 +4,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 747ecb800d..4e384bbcdb 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,9 +20,13 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
+extern void DiscardColumnEncryptionKeys(void);
+
 extern void debugStartup(DestReceiver *self, int operation,
 						 TupleDesc typeinfo);
 extern bool debugtup(TupleTableSlot *slot, DestReceiver *self);
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index ffd5e9dc82..eb59e73c0a 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -92,6 +92,9 @@ typedef enum ObjectClass
 	OCLASS_TYPE,				/* pg_type */
 	OCLASS_CAST,				/* pg_cast */
 	OCLASS_COLLATION,			/* pg_collation */
+	OCLASS_CEK,					/* pg_colenckey */
+	OCLASS_CEKDATA,				/* pg_colenckeydata */
+	OCLASS_CMK,					/* pg_colmasterkey */
 	OCLASS_CONSTRAINT,			/* pg_constraint */
 	OCLASS_CONVERSION,			/* pg_conversion */
 	OCLASS_DEFAULT,				/* pg_attrdef */
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index d01ab504b6..758696b539 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 3179be09d3..9e2c5256f7 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,9 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_colmasterkey.h',
+  'pg_colenckey.h',
+  'pg_colenckeydata.h',
 ]
 
 bki_data = [
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index f64a0ec26b..d0b9e8458d 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -115,6 +115,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c4d6adcd3e..c58b79e3a7 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5b950129de..0e9e85ebf3 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index b561e17781..7910175a6a 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,17 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	bool		attislocal BKI_DEFAULT(t);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey);
+
+	/*
+	 * User-visible type and typmod, currently used for encrypted columns.
+	 * These are only set to nondefault values if they are different from
+	 * atttypid and attypmod.
+	 */
+	Oid			attusertypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+	int32		attusertypmod BKI_DEFAULT(-1);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..c57fa18a27
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+/* ----------------
+ *		pg_colenckey definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckey
+ * ----------------
+ */
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+	Oid			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		cekacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_TOAST(pg_colenckey, 8263, 8264);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_nsp_index, 8242, ColumnEncKeyNameNspIndexId, on pg_colenckey using btree(cekname name_ops, ceknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkeydata.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+/* ----------------
+ *		pg_colenckeydata definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckeydata
+ * ----------------
+ */
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..d3bfd36279
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+	aclitem		cmkacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, on pg_colmasterkey using btree(cmkname name_ops, cmknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c867d99563..ff06d52fd0 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index b2cdea66c4..114279fa64 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 91587b99d0..c21052a3f7 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 505595620e..b410388a42 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6355,6 +6355,14 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -7142,6 +7150,68 @@
   proname => 'fmgr_sql_validator', provolatile => 's', prorettype => 'void',
   proargtypes => 'oid', prosrc => 'fmgr_sql_validator' },
 
+{ oid => '8265',
+  descr => 'user privilege on column encryption key by username, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_encryption_key_privilege_name_name' },
+{ oid => '8266',
+  descr => 'user privilege on column encryption key by username, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_encryption_key_privilege_name_id' },
+{ oid => '8267',
+  descr => 'user privilege on column encryption key by user oid, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_encryption_key_privilege_id_name' },
+{ oid => '8268',
+  descr => 'user privilege on column encryption key by user oid, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_encryption_key_privilege_id_id' },
+{ oid => '8269',
+  descr => 'current user privilege on column encryption key by column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_encryption_key_privilege_name' },
+{ oid => '8270',
+  descr => 'current user privilege on column encryption key by column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_encryption_key_privilege_id' },
+
+{ oid => '8271',
+  descr => 'user privilege on column master key by username, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_master_key_privilege_name_name' },
+{ oid => '8272',
+  descr => 'user privilege on column master key by username, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_master_key_privilege_name_id' },
+{ oid => '8273',
+  descr => 'user privilege on column master key by user oid, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_master_key_privilege_id_name' },
+{ oid => '8274',
+  descr => 'user privilege on column master key by user oid, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_master_key_privilege_id_id' },
+{ oid => '8275',
+  descr => 'current user privilege on column master key by column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_master_key_privilege_name' },
+{ oid => '8276',
+  descr => 'current user privilege on column master key by column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_master_key_privilege_id' },
+
 { oid => '2250',
   descr => 'user privilege on database by username, database name',
   proname => 'has_database_privilege', provolatile => 's', prorettype => 'bool',
@@ -11939,4 +12009,37 @@
   proname => 'any_value_transfn', prorettype => 'anyelement',
   proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8245',
+  proname => 'pg_encrypted_det_eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'pg_encrypted_det_ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 92bcaf2c73..b9d3920a97 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,17 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+# Note: typstorage 'e' since compression is not useful for encrypted data
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 519e570c8c..3c7ab2a8fe 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -294,6 +294,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPCATEGORY_USER		'U'
 #define  TYPCATEGORY_BITSTRING	'V' /* er ... "varbit"? */
 #define  TYPCATEGORY_UNKNOWN	'X'
+#define  TYPCATEGORY_ENCRYPTED	'Y'
 #define  TYPCATEGORY_INTERNAL	'Z'
 
 #define  TYPALIGN_CHAR			'c' /* char alignment (i.e. unaligned) */
diff --git a/src/include/commands/colenccmds.h b/src/include/commands/colenccmds.h
new file mode 100644
index 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/colenccmds.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COLENCCMDS_H
+#define COLENCCMDS_H
+
+#include "catalog/objectaddress.h"
+#include "parser/parse_node.h"
+
+extern ObjectAddress CreateCEK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt);
+extern ObjectAddress CreateCMK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e7c2b91a58..11f88c59ce 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,4 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml>.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index ac6407e9f6..39a286e58a 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -164,6 +164,7 @@ typedef struct Port
 	 */
 	char	   *database_name;
 	char	   *user_name;
+	bool		column_encryption_enabled;
 	char	   *cmdline_options;
 	List	   *guc_options;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f7d7f10f7d..1699e2a63f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -88,8 +88,7 @@ typedef uint64 AclMode;			/* a bitmask of privilege bits */
 #define ACL_REFERENCES	(1<<5)
 #define ACL_TRIGGER		(1<<6)
 #define ACL_EXECUTE		(1<<7)	/* for functions */
-#define ACL_USAGE		(1<<8)	/* for languages, namespaces, FDWs, and
-								 * servers */
+#define ACL_USAGE		(1<<8)	/* for various object types */
 #define ACL_CREATE		(1<<9)	/* for namespaces and databases */
 #define ACL_CREATE_TEMP (1<<10) /* for databases */
 #define ACL_CONNECT		(1<<11) /* for databases */
@@ -722,6 +721,7 @@ typedef struct ColumnDef
 	char	   *colname;		/* name of column */
 	TypeName   *typeName;		/* type of column */
 	char	   *compression;	/* compression method for column */
+	List	   *encryption;		/* encryption info for column */
 	int			inhcount;		/* number of times column is inherited */
 	bool		is_local;		/* column has local (non-inherited) def'n */
 	bool		is_not_null;	/* NOT NULL constraint specified? */
@@ -758,11 +758,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1980,6 +1981,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2167,6 +2171,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
@@ -3641,6 +3670,7 @@ typedef struct CheckPointStmt
 typedef enum DiscardMode
 {
 	DISCARD_ALL,
+	DISCARD_COLUMN_ENCRYPTION_KEYS,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
 	DISCARD_TEMP
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..c6da932042 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -149,6 +149,7 @@ PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("encryption", ENCRYPTION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -228,6 +229,7 @@ PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL)
 PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("language", LANGUAGE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("large", LARGE_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -250,6 +252,7 @@ PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("master", MASTER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index d4865e50f6..26e1cd617a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..a5e61c9f5b 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -29,6 +29,8 @@ PG_CMDTAG(CMDTAG_ALTER_ACCESS_METHOD, "ALTER ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_AGGREGATE, "ALTER AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CAST, "ALTER CAST", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_COLLATION, "ALTER COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY, "ALTER COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_MASTER_KEY, "ALTER COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONSTRAINT, "ALTER CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONVERSION, "ALTER CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_DATABASE, "ALTER DATABASE", false, false, false)
@@ -86,6 +88,8 @@ PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, fals
 PG_CMDTAG(CMDTAG_CREATE_AGGREGATE, "CREATE AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CAST, "CREATE CAST", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_COLLATION, "CREATE COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY, "CREATE COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_MASTER_KEY, "CREATE COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONSTRAINT, "CREATE CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONVERSION, "CREATE CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_DATABASE, "CREATE DATABASE", false, false, false)
@@ -130,6 +134,7 @@ PG_CMDTAG(CMDTAG_DECLARE_CURSOR, "DECLARE CURSOR", false, false, false)
 PG_CMDTAG(CMDTAG_DELETE, "DELETE", false, false, true)
 PG_CMDTAG(CMDTAG_DISCARD, "DISCARD", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
+PG_CMDTAG(CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS, "DISCARD COLUMN ENCRYPTION KEYS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false)
@@ -138,6 +143,8 @@ PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CAST, "DROP CAST", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_COLLATION, "DROP COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_ENCRYPTION_KEY, "DROP COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_MASTER_KEY, "DROP COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONSTRAINT, "DROP CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONVERSION, "DROP CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_DATABASE, "DROP DATABASE", false, false, false)
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2..0c73022833 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -159,6 +159,8 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
+#define ACL_ALL_RIGHTS_CEK			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_CMK			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..1dac24e4b4 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -163,6 +163,7 @@ extern bool type_is_rowtype(Oid typid);
 extern bool type_is_enum(Oid typid);
 extern bool type_is_range(Oid typid);
 extern bool type_is_multirange(Oid typid);
+extern bool type_is_encrypted(Oid typid);
 extern void get_type_category_preferred(Oid typid,
 										char *typcategory,
 										bool *typispreferred);
@@ -202,6 +203,9 @@ extern Oid	get_publication_oid(const char *pubname, bool missing_ok);
 extern char *get_publication_name(Oid pubid, bool missing_ok);
 extern Oid	get_subscription_oid(const char *subname, bool missing_ok);
 extern char *get_subscription_name(Oid subid, bool missing_ok);
+extern char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok);
+extern char *get_cmk_name(Oid cmkid, bool missing_ok);
 
 #define type_is_array(typid)  (get_element_type(typid) != InvalidOid)
 /* type_is_array_domain accepts both plain arrays and domains over arrays */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index a443181d41..8ecacb29df 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d5d50ceab4..bf1d527c1a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAMENSP,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAMENSP,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c18e914228..10dfda8016 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,7 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 8f80c35c89..df701f2df3 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -341,6 +341,14 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	{"cmklookup", "PGCMKLOOKUP", "", NULL,
+		"CMK-Lookup", "", 64,
+	offsetof(struct pg_conn, cmklookup)},
+
+	{"column_encryption", "PGCOLUMNENCRYPTION", "0", NULL,
+		"Column-Encryption", "", 1,
+	offsetof(struct pg_conn, column_encryption_setting)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -1412,6 +1420,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4056,6 +4086,22 @@ freePGconn(PGconn *conn)
 	free(conn->krbsrvname);
 	free(conn->gsslib);
 	free(conn->connip);
+	free(conn->cmklookup);
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		free(conn->cmks[i].cmkname);
+		free(conn->cmks[i].cmkrealm);
+	}
+	free(conn->cmks);
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		if (conn->ceks[i].cekdata)
+		{
+			explicit_bzero(conn->ceks[i].cekdata, conn->ceks[i].cekdatalen);
+			free(conn->ceks[i].cekdata);
+		}
+	}
+	free(conn->ceks);
 	/* Note that conn->Pfdebug is not ours to close or free */
 	free(conn->write_err_msg);
 	free(conn->inBuffer);
diff --git a/src/interfaces/libpq/fe-encrypt-openssl.c b/src/interfaces/libpq/fe-encrypt-openssl.c
new file mode 100644
index 0000000000..9baa47da13
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,839 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt-openssl.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe-encrypt.h"
+#include "libpq-int.h"
+
+#include "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+#define pqResultAlloc(res, nBytes, isBinary) malloc(nBytes)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = NULL;
+	EVP_PKEY_CTX *ctx = NULL;
+	unsigned char *out = NULL;
+	size_t		outlen;
+
+	switch (cmkalg)
+	{
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			md = EVP_sha1();
+			break;
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		free(out);
+		out = NULL;
+		goto fail;
+	}
+
+	*tolen = outlen;
+
+fail:
+	EVP_PKEY_CTX_free(ctx);
+	EVP_PKEY_free(key);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05>
+ * for encrypting and decrypting column values.
+ */
+
+#ifdef TEST_ENCRYPT
+
+/*
+ * Test data from
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5>
+ */
+
+/*
+ * The different test cases just use different prefixes of K, so one constant
+ * is enough here.
+ */
+static const unsigned char K[] = {
+	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+	0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+	0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+	0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+};
+
+static const unsigned char P[] = {
+	0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20,
+	0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75,
+	0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65,
+	0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62,
+	0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69,
+	0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66,
+	0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f,
+	0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65,
+};
+
+static const unsigned char test_IV[] = {
+	0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04,
+};
+
+static const unsigned char test_A[] = {
+	0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63,
+	0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20,
+	0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+static const EVP_CIPHER *
+pg_cekalg_to_openssl_cipher(int cekalg)
+{
+	const EVP_CIPHER *cipher;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			cipher = EVP_aes_128_cbc();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			cipher = EVP_aes_192_cbc();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			cipher = EVP_aes_256_cbc();
+			break;
+		default:
+			cipher = NULL;
+	}
+
+	return cipher;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+static const EVP_MD *
+pg_cekalg_to_openssl_md(int cekalg)
+{
+	const EVP_MD *md;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			md = EVP_sha384();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			md = EVP_sha512();
+			break;
+		default:
+			md = NULL;
+	}
+
+	return md;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+static int
+md_key_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 16;
+	else if (md == EVP_sha384())
+		return 24;
+	else if (md == EVP_sha512())
+		return 32;
+	else
+		return -1;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+static int
+md_hash_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 32;
+	else if (md == EVP_sha384())
+		return 48;
+	else if (md == EVP_sha512())
+		return 64;
+	else
+		return -1;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+static bool
+get_message_auth_tag(const EVP_MD *md,
+					 const unsigned char *mac_key, int mac_key_len,
+					 const unsigned char *encr, int encrlen,
+					 unsigned char *md_value, size_t *md_len_p,
+					 const char **errmsgp)
+{
+	static char msgbuf[1024];
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	int64		al;
+	bool		result = false;
+
+	if (encrlen < 0)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("encrypted value has invalid length"));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, mac_key, mac_key_len);
+	if (!pkey)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("could not allocate key for HMAC: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, buf, bufsize))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, md_len_p))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	Assert(*md_len_p == md_hash_length(md));
+
+	/* truncate output to half the length, per spec */
+	*md_len_p /= 2;
+
+	result = true;
+fail:
+	free(buf);
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+
+/*
+ * Decrypt a column value
+ */
+unsigned char *
+decrypt_value(PGresult *res, const PGCEK *cek, int cekalg, const unsigned char *input, int inputlen, const char **errmsgp)
+{
+	static char msgbuf[1024];
+
+	const unsigned char *iv = NULL;
+	size_t		ivlen;
+
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *decr;
+	int			decrlen,
+				decrlen2;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized encryption algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized digest algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("column encryption key has wrong length for algorithm (has: %zu, required: %d)"),
+				 cek->cekdatalen, key_len);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_memcmp(input + (inputlen - md_len), md_value, md_len) != 0)
+	{
+		*errmsgp = libpq_gettext("MAC mismatch");
+		goto fail;
+	}
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	iv = input;
+	input += ivlen;
+	inputlen -= ivlen;
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = inputlen + EVP_CIPHER_CTX_block_size(evp_cipher_ctx) + 1;
+	buf = pqResultAlloc(res, bufsize, false);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	decr = buf;
+	if (!EVP_DecryptUpdate(evp_cipher_ctx, decr, &decrlen, input, inputlen - md_len))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	if (!EVP_DecryptFinal_ex(evp_cipher_ctx, decr + decrlen, &decrlen2))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	decrlen += decrlen2;
+	Assert(decrlen < bufsize);
+	decr[decrlen] = '\0';
+	result = decr;
+
+fail:
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	return result;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#ifndef TEST_ENCRYPT
+static bool
+make_siv(PGconn *conn,
+		 unsigned char *iv, size_t ivlen,
+		 const EVP_MD *md,
+		 const unsigned char *iv_key, int iv_key_len,
+		 const unsigned char *plaintext, int plaintext_len)
+{
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	bool		result = false;
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, iv_key, iv_key_len);
+	if (!pkey)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	Assert(md_len == md_hash_length(md));
+	memcpy(iv, md_value, ivlen);
+
+	result = true;
+fail:
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+#endif							/* TEST_ENCRYPT */
+
+/*
+ * Encrypt a column value
+ */
+unsigned char *
+encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg, const unsigned char *value, int *nbytesp, bool enc_det)
+{
+	int			nbytes = *nbytesp;
+	unsigned char iv[EVP_MAX_IV_LENGTH];
+	size_t		ivlen;
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_key;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *encr;
+	int			encrlen,
+				encrlen2;
+
+	const char *errmsg;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		buf2size;
+	unsigned char *buf2 = NULL;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	Assert(ivlen <= sizeof(iv));
+	if (enc_det)
+	{
+#ifndef TEST_ENCRYPT
+		make_siv(conn, iv, ivlen, md, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	bufsize = ivlen + (nbytes + 2 * EVP_CIPHER_CTX_block_size(evp_cipher_ctx) - 1);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	encrlen += encrlen2;
+
+	encr -= ivlen;
+	encrlen += ivlen;
+
+	Assert(encrlen <= bufsize);
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  encr, encrlen,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf2, encr, encrlen);
+	memcpy(buf2 + encrlen, md_value, md_len);
+
+	result = buf2;
+	nbytes = buf2size;
+
+fail:
+	free(buf);
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	*nbytesp = nbytes;
+	return result;
+}
+
+
+/*
+ * Run test cases
+ */
+#ifdef TEST_ENCRYPT
+
+static void
+debug_print_hex(const char *name, const unsigned char *val, int len)
+{
+	printf("%s =", name);
+	for (int i = 0; i < len; i++)
+	{
+		if (i % 16 == 0)
+			printf("\n");
+		else
+			printf(" ");
+		printf("%02x", val[i]);
+	}
+	printf("\n");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, K_len);
+
+	C = encrypt_value(NULL, &cek, alg, P, &nbytes, true);
+	debug_print_hex("C", C, nbytes);
+}
+
+int
+main(int argc, char **argv)
+{
+	printf("5.1\n");
+	test_case(PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256, K, 32, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FE_ENCRYPT_H
+#define FE_ENCRYPT_H
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+extern unsigned char *decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+											int fromlen, const unsigned char *from,
+											int *tolen);
+
+extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg,
+									const unsigned char *input, int inputlen,
+									const char **errmsgp);
+
+extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg,
+									const unsigned char *value, int *nbytesp, bool enc_det);
+
+#endif							/* FE_ENCRYPT_H */
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index ec62550e38..128f8e3b96 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,420 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+int
+pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+					  const char *keyrealm)
+{
+	char	   *keyname_copy;
+	char	   *keyrealm_copy;
+	bool		found;
+
+	keyname_copy = strdup(keyname);
+	if (!keyname_copy)
+		return EOF;
+	keyrealm_copy = strdup(keyrealm);
+	if (!keyrealm_copy)
+	{
+		free(keyname_copy);
+		return EOF;
+	}
+
+	found = false;
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		struct pg_cmk *checkcmk = &conn->cmks[i];
+
+		/* replace existing? */
+		if (checkcmk->cmkid == keyid)
+		{
+			free(checkcmk->cmkname);
+			free(checkcmk->cmkrealm);
+			checkcmk->cmkname = keyname_copy;
+			checkcmk->cmkrealm = keyrealm_copy;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newncmks;
+		struct pg_cmk *newcmks;
+		struct pg_cmk *newcmk;
+
+		newncmks = conn->ncmks + 1;
+		if (newncmks <= 0)
+			return EOF;
+		newcmks = realloc(conn->cmks, newncmks * sizeof(struct pg_cmk));
+		if (!newcmks)
+		{
+			free(keyname_copy);
+			free(keyrealm_copy);
+			return EOF;
+		}
+
+		newcmk = &newcmks[newncmks - 1];
+		newcmk->cmkid = keyid;
+		newcmk->cmkname = keyname_copy;
+		newcmk->cmkrealm = keyrealm_copy;
+
+		conn->ncmks = newncmks;
+		conn->cmks = newcmks;
+	}
+
+	return 0;
+}
+
+/*
+ * Replace placeholders in input string.  Return value malloc'ed.
+ */
+static char *
+replace_cmk_placeholders(const char *in, const char *cmkname, const char *cmkrealm, int cmkalg, const char *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					p++;
+					break;
+				case 'r':
+					appendPQExpBufferStr(&buf, cmkrealm);
+					p++;
+					break;
+				default:
+					appendPQExpBufferChar(&buf, p[0]);
+			}
+		}
+		else
+			appendPQExpBufferChar(&buf, p[0]);
+	}
+
+	return buf.data;
+}
+
+#ifndef USE_SSL
+/*
+ * Dummy implementation for non-SSL builds
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	return NULL;
+}
+#endif
+
+/*
+ * Decrypt a CEK using the given CMK.  The ciphertext is passed in
+ * "from" and "fromlen".  Return the decrypted value in a malloc'ed area, its
+ * length via "tolen".  Return NULL on error; add error messages directly to
+ * "conn".
+ */
+static unsigned char *
+decrypt_cek(PGconn *conn, const PGCMK *cmk, int cmkalg,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	char	   *cmklookup;
+	bool		found = false;
+	unsigned char *result = NULL;
+
+	if (!conn->cmklookup || !conn->cmklookup[0])
+	{
+		libpq_append_conn_error(conn, "column master key lookup is not configured");
+		return NULL;
+	}
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		return NULL;
+	}
+
+	/*
+	 * Analyze semicolon-separated list
+	 */
+	for (char *s = strtok(cmklookup, ";"); s; s = strtok(NULL, ";"))
+	{
+		char	   *sep;
+
+		/* split found token at '=' */
+		sep = strchr(s, '=');
+		if (!sep)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', s);
+			break;
+		}
+
+		/* matching realm? */
+		if (strncmp(s, "*", sep - s) == 0 || strncmp(s, cmk->cmkrealm, sep - s) == 0)
+		{
+			char	   *sep2;
+
+			found = true;
+
+			sep2 = strchr(sep, ':');
+			if (!sep2)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, "r");
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+int
+pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, const unsigned char *value, int len)
+{
+	PGCMK	   *cmk = NULL;
+	unsigned char *plainval = NULL;
+	int			plainvallen = 0;
+	bool		found;
+
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		if (conn->cmks[i].cmkid == cmkid)
+		{
+			cmk = &conn->cmks[i];
+			break;
+		}
+	}
+
+	if (!cmk)
+		return EOF;
+
+	plainval = decrypt_cek(conn, cmk, cmkalg, len, value, &plainvallen);
+	if (!plainval)
+		return EOF;
+
+	found = false;
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		struct pg_cek *checkcek = &conn->ceks[i];
+
+		/* replace existing? */
+		if (checkcek->cekid == keyid)
+		{
+			free(checkcek->cekdata);
+			checkcek->cekdata = plainval;
+			checkcek->cekdatalen = plainvallen;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newnceks;
+		struct pg_cek *newceks;
+		struct pg_cek *newcek;
+
+		newnceks = conn->nceks + 1;
+		if (newnceks <= 0)
+		{
+			free(plainval);
+			return EOF;
+		}
+		newceks = realloc(conn->ceks, newnceks * sizeof(struct pg_cek));
+		if (!newceks)
+		{
+			free(plainval);
+			return EOF;
+		}
+
+		newcek = &newceks[newnceks - 1];
+		newcek->cekid = keyid;
+		newcek->cekdata = plainval;
+		newcek->cekdatalen = plainvallen;
+
+		conn->nceks = newnceks;
+		conn->ceks = newceks;
+	}
+
+	return 0;
+}
 
 /*
  * pqRowProcessor
@@ -1251,13 +1668,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == res->attDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					*errmsgp = libpq_gettext("protocol error: column encryption key associated with encrypted column was not sent by the server");
+					goto fail;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				if (val == NULL)
+					goto fail;
+#else
+				*errmsgp = libpq_gettext("column encryption not supported by this build");
 				goto fail;
+#endif
+			}
+			else
+			{
+				val = (char *) pqResultAlloc(res, clen + 1, isbinary);
+				if (val == NULL)
+					goto fail;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* copy and zero-terminate the data (even if it's binary) */
+				memcpy(val, columns[i].value, clen);
+				val[clen] = '\0';
+			}
 
 			tup[i].len = clen;
 			tup[i].value = val;
@@ -1500,6 +1955,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1973,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2012,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2128,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2171,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2270,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2319,47 @@ PQsendQueryGuts(PGconn *conn,
 		goto sendFailed;
 
 	/* Send parameter formats */
-	if (nParams > 0 && paramFormats)
+	if (nParams > 0 && (paramFormats || (paramDesc && paramDesc->paramDescs)))
 	{
 		if (pqPutInt(nParams, 2, conn) < 0)
 			goto sendFailed;
+
 		for (i = 0; i < nParams; i++)
 		{
-			if (pqPutInt(paramFormats[i], 2, conn) < 0)
+			int			format = paramFormats ? paramFormats[i] : 0;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2378,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2396,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				bool		enc_det = (paramDesc->paramDescs[i].flags & 0x01) != 0;
+				PGCEK	   *cek = NULL;
+				char	   *enc_paramValue;
+				int			enc_nbytes = nbytes;
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == paramDesc->paramDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					libpq_append_conn_error(conn, "protocol error: column encryption key associated with encrypted parameter was not sent by the server");
+					goto sendFailed;
+				}
+
+				enc_paramValue = (char *) encrypt_value(conn, cek, paramDesc->paramDescs[i].cekalg,
+														(const unsigned char *) paramValue, &enc_nbytes, enc_det);
+				if (!enc_paramValue)
+					goto sendFailed;
+
+				if (pqPutInt(enc_nbytes, 4, conn) < 0 ||
+					pqPutnchar(enc_paramValue, enc_nbytes, conn) < 0)
+					goto sendFailed;
+
+				free(enc_paramValue);
+#else
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2878,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4146,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4194,17 @@ PQfmod(const PGresult *res, int field_num)
 		return 0;
 }
 
+int
+PQfisencrypted(const PGresult *res, int field_num)
+{
+	if (!check_field_number(res, field_num))
+		return false;
+	if (res->attDescs)
+		return (res->attDescs[field_num].cekid != 0);
+	else
+		return false;
+}
+
 char *
 PQcmdStatus(PGresult *res)
 {
@@ -3762,6 +4390,17 @@ PQparamtype(const PGresult *res, int param_num)
 		return InvalidOid;
 }
 
+int
+PQparamisencrypted(const PGresult *res, int param_num)
+{
+	if (!check_param_number(res, param_num))
+		return false;
+	if (res->paramDescs)
+		return (res->paramDescs[param_num].cekid != 0);
+	else
+		return false;
+}
+
 
 /* PQsetnonblocking:
  *	sets the PGconn's database connection non-blocking if the arg is true
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 8ab6a88416..617e641cf6 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -43,6 +43,8 @@ static int	getRowDescriptions(PGconn *conn, int msgLength);
 static int	getParamDescriptions(PGconn *conn, int msgLength);
 static int	getAnotherTuple(PGconn *conn, int msgLength);
 static int	getParameterStatus(PGconn *conn);
+static int	getColumnMasterKey(PGconn *conn);
+static int	getColumnEncryptionKey(PGconn *conn);
 static int	getNotify(PGconn *conn);
 static int	getCopyStart(PGconn *conn, ExecStatusType copytype);
 static int	getReadyForQuery(PGconn *conn);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -358,8 +366,21 @@ pqParseInput3(PGconn *conn)
 					}
 					break;
 				case 't':		/* Parameter Description */
-					if (getParamDescriptions(conn, msgLength))
-						return;
+					if (conn->error_result ||
+						(conn->result != NULL &&
+						 conn->result->resultStatus == PGRES_FATAL_ERROR))
+					{
+						/*
+						 * We've already choked for some reason.  Just discard
+						 * the data till we get to the end of the query.
+						 */
+						conn->inCursor += msgLength;
+					}
+					else
+					{
+						if (getParamDescriptions(conn, msgLength))
+							return;
+					}
 					break;
 				case 'D':		/* Data Row */
 					if (conn->result != NULL &&
@@ -547,6 +568,9 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		int			typlen;
 		int			atttypmod;
 		int			format;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGets(&conn->workBuffer, conn) ||
 			pqGetInt(&tableid, 4, conn) ||
@@ -561,6 +585,23 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, conn) ||
+				pqGetInt(&flags, 2, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -582,6 +623,8 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		result->attDescs[i].typid = typid;
 		result->attDescs[i].typlen = typlen;
 		result->attDescs[i].atttypmod = atttypmod;
+		result->attDescs[i].cekid = cekid;
+		result->attDescs[i].cekalg = cekalg;
 
 		if (format != 1)
 			result->binary = 0;
@@ -685,10 +728,31 @@ getParamDescriptions(PGconn *conn, int msgLength)
 	for (i = 0; i < nparams; i++)
 	{
 		int			typid;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGetInt(&typid, 4, conn))
 			goto not_enough_data;
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&cekalg, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&flags, 2, conn))
+				goto not_enough_data;
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
 		result->paramDescs[i].typid = typid;
+		result->paramDescs[i].cekid = cekid;
+		result->paramDescs[i].cekalg = cekalg;
+		result->paramDescs[i].flags = flags;
 	}
 
 	/* Success! */
@@ -1468,6 +1532,92 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+/*
+ * Attempt to read a ColumnMasterKey message.
+ * Entry: 'y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	int			keyid;
+	char	   *keyname;
+	char	   *keyrealm;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	if (!keyname)
+		return EOF;
+	/* Get the key realm */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyrealm = strdup(conn->workBuffer.data);
+	if (!keyrealm)
+		return EOF;
+	/* And save it */
+	ret = pqSaveColumnMasterKey(conn, keyid, keyname, keyrealm);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(keyname);
+	free(keyrealm);
+
+	return ret;
+}
+
+/*
+ * Attempt to read a ColumnEncryptionKey message.
+ * Entry: 'Y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	int			keyid;
+	int			cmkid;
+	int			cmkalg;
+	char	   *buf;
+	int			vallen;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK ID */
+	if (pqGetInt(&cmkid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK algorithm */
+	if (pqGetInt(&cmkalg, 4, conn) != 0)
+		return EOF;
+	/* Get the key data len */
+	if (pqGetInt(&vallen, 4, conn) != 0)
+		return EOF;
+	/* Get the key data */
+	buf = malloc(vallen);
+	if (!buf)
+		return EOF;
+	if (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2436,9 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	if (conn->column_encryption_enabled)
+		ADD_STARTUP_OPTION("_pq_.column_encryption", "1");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index abaab6a073..77791e8349 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -450,7 +450,8 @@ pqTraceOutputS(FILE *f, const char *message, int *cursor)
 
 /* ParameterDescription */
 static void
-pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -458,12 +459,21 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 	nfields = pqTraceOutputInt16(f, message, cursor);
 
 	for (int i = 0; i < nfields; i++)
+	{
 		pqTraceOutputInt32(f, message, cursor, regress);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+			pqTraceOutputInt16(f, message, cursor);
+		}
+	}
 }
 
 /* RowDescription */
 static void
-pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -479,6 +489,11 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
 		pqTraceOutputInt16(f, message, cursor);
 		pqTraceOutputInt32(f, message, cursor, false);
 		pqTraceOutputInt16(f, message, cursor);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+		}
 	}
 }
 
@@ -514,6 +529,30 @@ pqTraceOutputW(FILE *f, const char *message, int *cursor, int length)
 		pqTraceOutputInt16(f, message, cursor);
 }
 
+/* ColumnMasterKey */
+static void
+pqTraceOutputy(FILE *f, const char *message, int *cursor, bool regress)
+{
+	fprintf(f, "ColumnMasterKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+}
+
+/* ColumnEncryptionKey */
+static void
+pqTraceOutputY(FILE *f, const char *message, int *cursor, bool regress)
+{
+	int			len;
+
+	fprintf(f, "ColumnEncryptionKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, false);
+	len = pqTraceOutputInt32(f, message, cursor, false);
+	pqTraceOutputNchar(f, len, message, cursor);
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -647,10 +686,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 				fprintf(conn->Pfdebug, "Sync"); /* no message content */
 			break;
 		case 't':				/* Parameter Description */
-			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'T':				/* Row Description */
-			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'v':				/* Negotiate Protocol Version */
 			pqTraceOutputv(conn->Pfdebug, message, &logCursor);
@@ -665,6 +706,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			fprintf(conn->Pfdebug, "Terminate");
 			/* No message content */
 			break;
+		case 'y':
+			pqTraceOutputy(conn->Pfdebug, message, &logCursor, regress);
+			break;
+		case 'Y':
+			pqTraceOutputY(conn->Pfdebug, message, &logCursor, regress);
+			break;
 		case 'Z':				/* Ready For Query */
 			pqTraceOutputZ(conn->Pfdebug, message, &logCursor);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f3d9220496..cf339a3e53 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -267,6 +267,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -438,6 +440,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,7 @@ extern int	PQfformat(const PGresult *res, int field_num);
 extern Oid	PQftype(const PGresult *res, int field_num);
 extern int	PQfsize(const PGresult *res, int field_num);
 extern int	PQfmod(const PGresult *res, int field_num);
+extern int	PQfisencrypted(const PGresult *res, int field_num);
 extern char *PQcmdStatus(PGresult *res);
 extern char *PQoidStatus(const PGresult *res);	/* old and ugly */
 extern Oid	PQoidValue(const PGresult *res);	/* new and improved */
@@ -540,6 +559,7 @@ extern int	PQgetlength(const PGresult *res, int tup_num, int field_num);
 extern int	PQgetisnull(const PGresult *res, int tup_num, int field_num);
 extern int	PQnparams(const PGresult *res);
 extern Oid	PQparamtype(const PGresult *res, int param_num);
+extern int	PQparamisencrypted(const PGresult *res, int param_num);
 
 /* Describe prepared statements and portals */
 extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d7ec5ed429..cbfaa7f95f 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -112,6 +112,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -343,6 +346,26 @@ typedef struct pg_conn_host
 								 * found in password file. */
 } pg_conn_host;
 
+/*
+ * Column encryption support data
+ */
+
+/* column master key */
+typedef struct pg_cmk
+{
+	Oid			cmkid;
+	char	   *cmkname;
+	char	   *cmkrealm;
+} PGCMK;
+
+/* column encryption key */
+typedef struct pg_cek
+{
+	Oid			cekid;
+	unsigned char *cekdata;		/* (decrypted) */
+	size_t		cekdatalen;
+} PGCEK;
+
 /*
  * PGconn stores all the state data associated with a single connection
  * to a backend.
@@ -396,6 +419,8 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *cmklookup;		/* CMK lookup specification */
+	char	   *column_encryption_setting;	/* column_encryption connection parameter (0 or 1) */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -477,6 +502,13 @@ struct pg_conn
 	PGVerbosity verbosity;		/* error/notice message verbosity */
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
+	bool		column_encryption_enabled;	/* parsed version of column_encryption_setting */
+
+	/* Column encryption support data */
+	int			ncmks;
+	PGCMK	   *cmks;
+	int			nceks;
+	PGCEK	   *ceks;
 
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
@@ -673,6 +705,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern int	pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+								  const char *keyrealm);
+extern int	pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg,
+									  const unsigned char *value, int len);
 extern int	pqRowProcessor(PGconn *conn, const char **errmsgp);
 extern void pqCommandQueueAdvance(PGconn *conn);
 extern int	PQsendQueryContinue(PGconn *conn, const char *query);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 573fd9b6ea..a6e6b2ada9 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,7 @@ endif
 
 if ssl.found()
   libpq_sources += files('fe-secure-common.c')
+  libpq_sources += files('fe-encrypt-openssl.c')
   libpq_sources += files('fe-secure-openssl.c')
 endif
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': get_option('ssl')},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 4df544ecef..0c36aa5f32 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
+GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-encrypt-openssl.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
 GETTEXT_TRIGGERS = libpq_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext pqInternalNotice:2
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 100644
--- a/src/interfaces/libpq/test/Makefile
+++ b/src/interfaces/libpq/test/Makefile
@@ -15,10 +15,17 @@ override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
 LDFLAGS_INTERNAL += $(libpq_pgport)
 
 PROGS = libpq_testclient libpq_uri_regress
+ifeq ($(with_ssl),openssl)
+PROGS += libpq_test_encrypt
+endif
+
 
 all: $(PROGS)
 
 $(PROGS): $(WIN32RES)
 
+libpq_test_encrypt: ../fe-encrypt-openssl.c
+	$(CC) $(CPPFLAGS) -DTEST_ENCRYPT $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 clean distclean maintainer-clean:
 	rm -f $(PROGS) *.o
diff --git a/src/interfaces/libpq/test/meson.build b/src/interfaces/libpq/test/meson.build
index b2a4b06fd2..87d2808b52 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c8ba170503 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -24,7 +24,7 @@ ifeq ($(with_ldap),yes)
 SUBDIRS += ldap
 endif
 ifeq ($(with_ssl),openssl)
-SUBDIRS += ssl
+SUBDIRS += column_encryption ssl
 endif
 
 # Test suites that are not safe by default but can be run if selected
@@ -36,7 +36,7 @@ export PG_TEST_EXTRA
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl column_encryption)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..d5ead874e5
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/column_encryption/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+export OPENSSL PERL
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/meson.build b/src/test/column_encryption/meson.build
new file mode 100644
index 0000000000..47f88c41ce
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,24 @@
+column_encryption_test_client = executable('test_client',
+  files('test_client.c'),
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+testprep_targets += column_encryption_test_client
+
+tests += {
+  'name': 'column_encryption',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_column_encryption.pl',
+      't/002_cmk_rotation.pl',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+      'PERL': perl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..c56af737ac
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,255 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+my $perl = $ENV{PERL};
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$openssl, 'pkeyutl', '-encrypt',
+		'-inkey', $cmkfilename,
+		'-pkeyopt', 'rsa_padding_mode:oaep',
+		'-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+		'-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+	# create CEK in database
+	$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = ${cmkname}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c smallint ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# Expected ciphertext length is 2 blocks of AES output (2 * 16) plus
+# half SHA-256 output (16) in hex encoding: (2 * 16 + 16) * 2 = 96
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1) TO STDOUT}),
+	qr/1\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text
+c|smallint),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result');
+
+{
+	local $ENV{PGCMKLOOKUP} = '*=run:broken %k %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = "*=run:$perl ./test_run_decrypt.pl %k %a %p";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$node->command_fails_like(['test_client', 'test1'], qr/not encrypted/, 'test client fails because parameters not encrypted');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result after test client insert');
+
+$node->command_ok(['test_client', 'test2'], 'test client test 2');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3|33),
+	'decrypted query result after test client insert 2');
+
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1 WHERE a = 3) TO STDOUT}),
+	qr/3\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3upd|33),
+	'decrypted query result after update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+is($node->safe_psql('postgres', q{SELECT b, count(*) FROM tbl2 GROUP BY b ORDER BY 2}),
+	q(valB|1
+valA|2),
+	'group by deterministically encrypted column');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl3 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c text ENCRYPTED WITH (column_encryption_key = cek2, algorithm = 'AEAD_AES_192_CBC_HMAC_SHA_384')
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1
+2|valB2|valC2
+3|valB3|valC3),
+	'decrypted query result multiple keys after second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..14eafb8ec9
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Test column master key rotation.  First, we generate CMK1 and a CEK
+# encrypted with it.  Then we add a CMK2 and encrypt the CEK with it
+# as well.  (Recall that a CEK can be associated with multiple CMKs,
+# for this reason.  That's why pg_colenckeydata is split out from
+# pg_colenckey.)  Then we remove CMK1.  We test that we can get
+# decrypted query results at each step.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# generate random bytes
+system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+# encrypt CEK using CMK
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk1filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# create CEK in database
+$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = cmk1, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b) VALUES (1, $1) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with one CMK');
+
+
+# create new CMK
+my $cmk2filename = create_cmk('cmk2');
+
+# encrypt CEK using new CMK
+#
+# (Here, we still have the plaintext of the CEK available from
+# earlier.  In reality, one would decrypt the CEK with the first CMK
+# and then re-encrypt it with the second CMK.)
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk2filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+$cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# add new data record for CEK in database
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} ADD VALUE (column_master_key = cmk2, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with two CMKs');
+
+
+# delete CEK record for first CMK
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} DROP VALUE (column_master_key = cmk1);});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with only new CMK');
+
+
+done_testing();
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (!(!PQparamisencrypted(res2, 0) &&
+		  PQparamisencrypted(res2, 1)))
+	{
+		fprintf(stderr, "wrong results from PQparamisencrypted()\n");
+		return 1;
+	}
+
+	res = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	PGconn	   *conn;
+	int			ret = 0;
+
+	conn = PQconnectdb("");
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		fprintf(stderr, "Connection to database failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (argc < 2 || argv[1] == NULL)
+		return 87;
+	else if (strcmp(argv[1], "test1") == 0)
+		ret = test1(conn);
+	else if (strcmp(argv[1], "test2") == 0)
+		ret = test2(conn);
+	else if (strcmp(argv[1], "test3") == 0)
+		ret = test3(conn);
+	else if (strcmp(argv[1], "test4") == 0)
+		ret = test4(conn);
+	else
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+
+# Test/sample command for libpq cmklookup run scheme
+#
+# This just places the data into temporary files and runs the openssl
+# command on it.  (In practice, this could more simply be written as a
+# shell script, but this way it's more portable.)
+
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $fh, '<:raw', "${tmpdir}/output.tmp" or die $!;
+my $data = '';
+
+while (1) {
+	my $success = read $fh, $data, 100, length($data);
+	die $! if not defined $success;
+	last if not $success;
+}
+
+close $fh;
+
+unlink "${tmpdir}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..d55ddb1ab7 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,7 @@ subdir('subscription')
 subdir('modules')
 
 if ssl.found()
+  subdir('column_encryption')
   subdir('ssl')
 endif
 
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..df47e36280
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,451 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE ROLE regress_enc_user1;
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+CREATE COLUMN MASTER KEY cmk2;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "cek1" already has data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "fail" does not exist
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+ERROR:  column encryption key "notexist" does not exist
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+\d tbl_29f3
+              Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_29f3
+                                        Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+\d tbl_447f
+              Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_447f
+                                        Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  permission denied for column master key cmk1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+ERROR:  permission denied for column encryption key cek1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column master key cmk1 depends on column master key cmk1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ERROR:  column master key "cmk3" already exists in schema "public"
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+ERROR:  column master key "cmkx" does not exist
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ERROR:  column encryption key "cek3" already exists in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+ERROR:  must be owner of column encryption key cek3
+DROP COLUMN MASTER KEY cmk3;  -- fail
+ERROR:  must be owner of column master key cmk3
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ERROR:  column encryption key "cek1" has no data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ERROR:  column master key "fail" does not exist
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+ERROR:  attribute "algorithm" must not be specified
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+ERROR:  column encryption key "fail" does not exist
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf..77fedce8aa 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -107,7 +109,8 @@ BEGIN
         ('text search template'), ('text search configuration'),
         ('policy'), ('user mapping'), ('default acl'), ('transform'),
         ('operator of access method'), ('function of access method'),
-        ('publication namespace'), ('publication relation')
+        ('publication namespace'), ('publication relation'),
+        ('column encryption key'), ('column encryption key data'), ('column master key')
     LOOP
         FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
         LOOP
@@ -327,6 +330,24 @@ WARNING:  error for publication relation,{addr_nsp,zwei},{}: argument list lengt
 WARNING:  error for publication relation,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist
 WARNING:  error for publication relation,{eins,zwei,drei},{}: argument list length must be exactly 1
 WARNING:  error for publication relation,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei"
+WARNING:  error for column encryption key,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins},{}: column master key "eins" does not exist
+WARNING:  error for column master key,{eins},{integer}: column master key "eins" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 -- these object types cannot be qualified names
 SELECT pg_get_object_address('language', '{one}', '{}');
 ERROR:  language "one" does not exist
@@ -409,6 +430,9 @@ WITH objects (type, name, args) AS (VALUES
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,8 @@ drop cascades to user mapping for regress_addr_user on server integer
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 DROP SCHEMA addr_nsp CASCADE;
 NOTICE:  drop cascades to 14 other objects
 DETAIL:  drop cascades to text search dictionary addr_ts_dict
@@ -547,6 +576,9 @@ WITH objects (classid, objid, objsubid) AS (VALUES
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|NULL
+("(""column master key"",,,)")|("(""column master key"",,)")|NULL
+("(""column encryption key"",,,)")|("(""column encryption key"",,)")|NULL
+("(""column encryption key data"",,,)")|("(""column encryption key data"",,)")|NULL
 -- restore normal output mode
 \a\t
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..226f5e404e 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attusertypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,9 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,13 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
  anyrange                    | anymultirange
-(5 rows)
+(6 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -872,6 +876,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+pg_encrypted_det_eq(pg_encrypted_det,pg_encrypted_det)
+pg_encrypted_det_ne(pg_encrypted_det,pg_encrypted_det)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -75,7 +75,9 @@ ORDER BY t1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | pg_encrypted_det
+ 8244 | pg_encrypted_rnd
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT t1.oid, t1.typname as basetype, t2.typname as arraytype,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 15e015b3d6..577a6d4b2e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 427429975e..57c7d2bec3 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..8d6320015c
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,297 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE ROLE regress_enc_user1;
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk2;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+\d tbl_29f3
+\d+ tbl_29f3
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+
+\d tbl_447f
+\d+ tbl_447f
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+
+
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+
+
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..35af43032f 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -99,7 +101,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
         ('text search template'), ('text search configuration'),
         ('policy'), ('user mapping'), ('default acl'), ('transform'),
         ('operator of access method'), ('function of access method'),
-        ('publication namespace'), ('publication relation')
+        ('publication namespace'), ('publication relation'),
+        ('column encryption key'), ('column encryption key data'), ('column master key')
     LOOP
         FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
         LOOP
@@ -174,6 +177,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -228,6 +234,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 
 DROP SCHEMA addr_nsp CASCADE;
 
@@ -247,6 +255,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 71a75626d5271f2bcdbdc43b8c13065c4634fd9f
-- 
2.39.2



^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* Re: Transparent column encryption
@ 2023-03-10 07:18  Peter Eisentraut <[email protected]>
  parent: Peter Eisentraut <[email protected]>
  0 siblings, 2 replies; 62+ messages in thread

From: Peter Eisentraut @ 2023-03-10 07:18 UTC (permalink / raw)
  To: pgsql-hackers

Here is an updated patch.  I have done some cosmetic polishing and fixed 
a minor Windows-related bug.

In my mind, the patch is complete.

If someone wants to do some in-depth code review, I suggest focusing on 
the following files:

* src/backend/access/common/printtup.c
* src/backend/commands/colenccmds.c
* src/backend/commands/tablecmds.c
* src/backend/parser/parse_param.c
* src/interfaces/libpq/fe-exec.c
* src/interfaces/libpq/fe-protocol3.c
* src/interfaces/libpq/libpq-fe.h

(Most other files are DDL boilerplate or otherwise not too interesting.)

From c73c3f5988aa8c96b7f480916c0eac50ea8b6c1c Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Fri, 10 Mar 2023 08:07:00 +0100
Subject: [PATCH v18] Automatic client-side column-level encryption

This feature enables the automatic encryption and decryption of
particular columns in the client.  The data for those columns then
only ever appears in ciphertext on the server, so it is protected from
DBAs, sysadmins, cloud operators, etc. as well as accidental leakage
to server logs, file-system backups, etc.  The canonical use case for
this feature is storing credit card numbers encrypted, in accordance
with PCI DSS, as well as similar situations involving social security
numbers etc.  One can't do any computations with encrypted values on
the server, but for these use cases, that is not necessary.  This
feature does support deterministic encryption as an alternative to the
default randomized encryption, so in that mode one can do equality
lookups, at the cost of some security.

This functionality also exists in other database products, and the
overall concepts were mostly adopted from there.

(Note: This feature has nothing to do with any on-disk encryption
feature.  Both can exist independently.)

You declare a column as encrypted in a CREATE TABLE statement.  The
column value is encrypted by a symmetric key called the column
encryption key (CEK).  The CEK is a catalog object.  The CEK key
material is in turn encrypted by an asymmetric key called the column
master key (CMK).  The CMK is not stored in the database but somewhere
where the client can get to it, for example in a file or in a key
management system.  When a server sends rows containing encrypted
column values to the client, it first sends the required CMK and CEK
information (new protocol messages), which the client needs to record.
Then, the client can use this information to automatically decrypt the
incoming row data and forward it in plaintext to the application.

For the CMKs, libpq has a new connection parameter "cmklookup" that
specifies via a mini-language where to get the keys.  Right now, you
can use "file" to read it from a file, or "run" to run some program,
which could get it from a KMS.

The general idea would be for an application to have one CMK per area
of secret stuff, for example, for credit card data.  The CMK can be
rotated: each CEK can be represented multiple times in the database,
encrypted by a different CMK.  (The CEK can't be rotated easily, since
that would require reading out all the data from a table/column and
reencrypting it.  We could/should add some custom tooling for that,
but it wouldn't be a routine operation.)

Several encryption algorithms are provided.  The CMK process uses
RSAES_OAEP_SHA_1 or _256.  The CEK process uses
AEAD_AES_*_CBC_HMAC_SHA_* with several strengths.

In the server, the encrypted datums are stored in types called
pg_encrypted_rnd and pg_encrypted_det (for randomized and
deterministic encryption).  These are essentially cousins of bytea.
For the rest of the database system below the protocol handling, there
is nothing special about those.  For example, pg_encrypted_rnd has no
operators at all, pg_encrypted_det has only an equality operator.
pg_attribute has a new column attrealtypid that stores the original
type of the data in the column.  This is only used for providing it to
clients, so that higher-level clients can convert the decrypted value
to their appropriate data types in their environments.

The protocol extensions are guarded by a new protocol extension option
"_pq_.column_encryption".  If this is not set, nothing changes, the
protocol stays the same, and no encryption or decryption happens.

To get automatically encrypted data into the database (as opposed to
reading it out), it is required to use protocol-level prepared
statements (i.e., extended query).  The client must first prepare a
statement, then describe the statement to get parameter metadata,
which indicates which parameters are to be encrypted and how.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this if the
\bind command is used.

Another challenge is that the parse analysis must check which
underlying column a parameter corresponds to.  This is similar to
resorigtbl and resorigcol in the opposite direction.

Discussion: https://www.postgresql.org/message-id/flat/89157929-c2b6-817b-6025-8e4b2d89d88f%40enterprisedb.com
---
 doc/src/sgml/acronyms.sgml                    |  18 +
 doc/src/sgml/catalogs.sgml                    | 317 +++++++
 doc/src/sgml/charset.sgml                     |  10 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 444 +++++++++
 doc/src/sgml/func.sgml                        |  60 ++
 doc/src/sgml/glossary.sgml                    |  26 +
 doc/src/sgml/libpq.sgml                       | 322 +++++++
 doc/src/sgml/protocol.sgml                    | 467 ++++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |  10 +
 .../ref/create_column_encryption_key.sgml     | 173 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  55 +-
 doc/src/sgml/ref/discard.sgml                 |  14 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/grant.sgml                   |  12 +-
 doc/src/sgml/ref/pg_dump.sgml                 |  42 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   8 +
 src/backend/access/common/printtup.c          | 237 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  60 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 288 ++++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  17 +
 src/backend/commands/colenccmds.c             | 439 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/discard.c                |   8 +-
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 197 ++++
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 200 ++++-
 src/backend/parser/parse_param.c              | 157 ++++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  56 ++
 src/backend/utils/adt/acl.c                   | 398 +++++++++
 src/backend/utils/adt/varlena.c               | 107 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  42 +
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/dumputils.c                   |   4 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 377 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  35 +
 src/bin/pg_dump/pg_dump_sort.c                |  14 +
 src/bin/pg_dump/pg_dumpall.c                  |   5 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  72 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 191 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  69 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   4 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |  11 +
 src/include/catalog/pg_colenckey.h            |  46 +
 src/include/catalog/pg_colenckeydata.h        |  46 +
 src/include/catalog/pg_colmasterkey.h         |  47 +
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  10 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               | 103 +++
 src/include/catalog/pg_type.dat               |  13 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  41 +-
 src/include/parser/kwlist.h                   |   3 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   7 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 839 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 671 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 157 +++-
 src/interfaces/libpq/fe-trace.c               |  56 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   1 +
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  23 +
 .../t/001_column_encryption.pl                | 257 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 451 ++++++++++
 src/test/regress/expected/object_address.out  |  35 +
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 297 +++++++
 src/test/regress/sql/object_address.sql       |  11 +
 src/test/regress/sql/type_sanity.sql          |   2 +
 144 files changed, 10393 insertions(+), 84 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/alter_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_master_key.sgml
 create mode 100644 src/backend/commands/colenccmds.c
 create mode 100644 src/common/colenc.c
 create mode 100644 src/include/catalog/pg_colenckey.h
 create mode 100644 src/include/catalog/pg_colenckeydata.h
 create mode 100644 src/include/catalog/pg_colmasterkey.h
 create mode 100644 src/include/commands/colenccmds.h
 create mode 100644 src/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 create mode 100644 src/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/meson.build
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/t/002_cmk_rotation.pl
 create mode 100644 src/test/column_encryption/test_client.c
 create mode 100755 src/test/column_encryption/test_run_decrypt.pl
 create mode 100644 src/test/regress/expected/column_encryption.out
 create mode 100644 src/test/regress/sql/column_encryption.sql

diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 2df6559acc..3a5f2c254c 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -56,6 +56,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CEK</acronym></term>
+    <listitem>
+     <para>
+      Column Encryption Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CIDR</acronym></term>
     <listitem>
@@ -67,6 +76,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CMK</acronym></term>
+    <listitem>
+     <para>
+      Column Master Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CPAN</acronym></term>
     <listitem>
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 746baf5053..54c5199e0f 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -105,6 +105,21 @@ <title>System Catalogs</title>
       <entry>collations (locale information)</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link></entry>
+      <entry>column encryption keys</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link></entry>
+      <entry>column encryption key data</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link></entry>
+      <entry>column master keys</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link></entry>
       <entry>check constraints, unique constraints, primary key constraints, foreign key constraints</entry>
@@ -1360,6 +1375,44 @@ <title><structname>pg_attribute</structname> Columns</title>
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attcek</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, a reference to the column encryption key, else 0.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type of the
+       encrypted data that is reported to the client.  If the column is not
+       encrypted, then 0.  For encrypted columns, the field
+       <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.
+       </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type
+       modifier (analogous to <structfield>atttypmod</structfield>) that is
+       reported to the client.  If the column is not encrypted, then -1.  For
+       encrypted columns, the field <structfield>atttypmod</structfield>)
+       contains the identifier of the encryption algorithm; see <xref
+       linkend="protocol-cek"/> for possible values.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>attinhcount</structfield> <type>int4</type>
@@ -2476,6 +2529,270 @@ <title><structname>pg_collation</structname> Columns</title>
   </para>
  </sect1>
 
+ <sect1 id="catalog-pg-colenckey">
+  <title><structname>pg_colenckey</structname></title>
+
+  <indexterm zone="catalog-pg-colenckey">
+   <primary>pg_colenckey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckey</structname> contains information
+   about the column encryption keys in the database.  The actual key material
+   of the column encryption keys is in the catalog <link
+   linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link>.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column encryption key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colenckeydata">
+  <title><structname>pg_colenckeydata</structname></title>
+
+  <indexterm zone="catalog-pg-colenckeydata">
+   <primary>pg_colenckeydata</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckeydata</structname> contains the key
+   material of column encryption keys.  Each column encryption key object can
+   contain several versions of the key material, each encrypted with a
+   different column master key.  That allows the gradual rotation of the
+   column master keys.  Thus, <literal>(ckdcekid, ckdcmkid)</literal> is a
+   unique key of this table.
+  </para>
+
+  <para>
+   The key material of column encryption keys should never be decrypted inside
+   the database instance.  It is meant to be sent as-is to the client, where
+   it is decrypted using the associated column master key, and then used to
+   encrypt or decrypt column values.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckeydata</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcekid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column encryption key this entry belongs to
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column master key that the key material is encrypted with
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkalg</structfield> <type>int4</type>
+      </para>
+      <para>
+       The encryption algorithm used for encrypting the key material; see
+       <xref linkend="protocol-cmk"/> for possible values.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdencval</structfield> <type>bytea</type>
+      </para>
+      <para>
+       The key material of this column encryption key, encrypted using the
+       referenced column master key
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colmasterkey">
+  <title><structname>pg_colmasterkey</structname></title>
+
+  <indexterm zone="catalog-pg-colmasterkey">
+   <primary>pg_colmasterkey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colmasterkey</structname> contains information
+   about column master keys.  The keys themselves are not stored in the
+   database.  The catalog entry only contains information that is used by
+   clients to locate the keys, for example in a file or in a key management
+   system.
+  </para>
+
+  <table>
+   <title><structname>pg_colmasterkey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column master key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkrealm</structfield> <type>text</type>
+      </para>
+      <para>
+       A <quote>realm</quote> associated with this column master key.  This is
+       a freely chosen string that is used by clients to determine how to look
+       up the key.  A typical configuration would put all CMKs that are looked
+       up in the same way into the same realm.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml
index 3032392b80..f3026fff83 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,16 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When automatic client-side column-level encryption is used, then no
+     encoding conversion is possible.  (The encoding conversion happens on the
+     server, and the server cannot look inside any encrypted column values.)
+     If automatic client-side column-level encryption is enabled for a
+     session, then the server enforces that the client encoding matches the
+     server encoding, and any attempts to change the client encoding will be
+     rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..67fce16872 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <title>Pseudo-Types</title>
 
   </sect1>
 
+  <sect1 id="datatype-encrypted">
+   <title>Types Related to Encryption</title>
+
+   <para>
+    An encrypted column value (see <xref linkend="ddl-column-encryption"/>) is
+    internally stored using the types
+    <type>pg_encrypted_rnd</type> (for randomized encryption) or
+    <type>pg_encrypted_det</type> (for deterministic encryption); see <xref
+    linkend="datatype-encrypted-table"/>.  Most of the database system treats
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support automatic client-side column-level encryption
+    or have disabled it will see the encrypted values in this format.  Clients
+    that support automatic client-side column-level encryption will not see
+    these types in result sets, as the protocol layer will translate them back
+    to the declared underlying type in the table definition.
+   </para>
+
+    <table id="datatype-encrypted-table">
+     <title>Types Related to Encryption</title>
+     <tgroup cols="3">
+      <colspec colname="col1" colwidth="1*"/>
+      <colspec colname="col2" colwidth="3*"/>
+      <colspec colname="col3" colwidth="2*"/>
+      <thead>
+       <row>
+        <entry>Name</entry>
+        <entry>Storage Size</entry>
+        <entry>Description</entry>
+       </row>
+      </thead>
+      <tbody>
+       <row>
+        <entry><type>pg_encrypted_det</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, deterministic encryption</entry>
+       </row>
+       <row>
+        <entry><type>pg_encrypted_rnd</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, randomized encryption</entry>
+       </row>
+      </tbody>
+     </tgroup>
+    </table>
+  </sect1>
  </chapter>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 5179125510..65514e119f 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,440 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   With <firstterm>automatic client-side column-level encryption</firstterm>,
+   columns can be stored encrypted in the database.  The encryption and
+   decryption happens automatically on the client, so that the plaintext value
+   is never seen in the database instance or on the server hosting the
+   database.  The drawback is that most operations, such as function calls or
+   sorting, are not possible on encrypted values.
+  </para>
+
+  <sect2>
+   <title>Using Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   Automatic client-side column-level encryption uses two levels of
+   cryptographic keys.  The actual column value is encrypted using a symmetric
+   algorithm, such as AES, using a <firstterm>column encryption
+   key</firstterm> (<acronym>CEK</acronym>).  The column encryption key is in
+   turn encrypted using an asymmetric algorithm, such as RSA, using a
+   <firstterm>column master key</firstterm> (<acronym>CMK</acronym>).  The
+   encrypted CEK is stored in the database system.  The CMK is not stored in
+   the database system; it is stored on the client or somewhere where the
+   client can access it, such as in a local file or in a key management
+   system.  The database system only records where the CMK is stored and
+   provides this information to the client.  When rows containing encrypted
+   columns are sent to the client, the server first sends any necessary CMK
+   information, followed by any required CEK.  The client then looks up the
+   CMK and uses that to decrypt the CEK.  Then it decrypts incoming row data
+   using the CEK and provides the decrypted row data to the application.
+  </para>
+
+  <para>
+   Here is an example declaring a column as encrypted:
+<programlisting>
+CREATE TABLE customers (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    creditcard_num text <emphasis>ENCRYPTED WITH (column_encryption_key = cek1)</emphasis>
+);
+</programlisting>
+  </para>
+
+  <para>
+   Column encryption supports <firstterm>randomized</firstterm>
+   (also known as <firstterm>probabilistic</firstterm>) and
+   <firstterm>deterministic</firstterm> encryption.  The above example uses
+   randomized encryption, which is the default.  Randomized encryption uses a
+   random initialization vector for each encryption, so that even if the
+   plaintext of two rows is equal, the encrypted values will be different.
+   This prevents someone with direct access to the database server from making
+   computations such as distinct counts on the encrypted values.
+   Deterministic encryption uses a fixed initialization vector.  This reduces
+   security, but it allows equality searches on encrypted values.  The
+   following example declares a column with deterministic encryption:
+<programlisting>
+CREATE TABLE employees (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    ssn text ENCRYPTED WITH (
+        column_encryption_key = cek1, <emphasis>encryption_type = deterministic</emphasis>)
+);
+</programlisting>
+  </para>
+
+  <para>
+   Null values are not encrypted by automatic client-side column-level
+   encryption; null values sent by the client are visible as null values in
+   the database.  If the fact that a value is null needs to be hidden from the
+   server, this information needs to be encoded into a nonnull value in the
+   client somehow.
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Reading and Writing Encrypted Columns</title>
+
+   <para>
+    Reading and writing encrypted columns is meant to be handled automatically
+    by the client library/driver and should be mostly transparent to the
+    application code, if certain prerequisites are fulfilled:
+
+    <itemizedlist>
+     <listitem>
+      <para>
+       The client library needs to support automatic client-side column-level
+       encryption.  Not all client libraries do.  Furthermore, the client
+       library might require that automatic client-side column-level
+       encryption is explicitly enabled at connection time.  See the
+       documentation of the client library for details.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Column master keys and column encryption keys have been set up, and the
+       client library has been configured to be able to look up column master
+       keys from the key store or key management system.
+      </para>
+     </listitem>
+    </itemizedlist>
+   </para>
+
+   <para>
+    Reading from encrypted columns will then work automatically.  For example,
+    using the above example,
+<programlisting>
+SELECT ssn FROM employees WHERE id = 5;
+</programlisting>
+    would return the unencrypted value for the <literal>ssn</literal> column
+    in any rows found.
+   </para>
+
+   <para>
+    Writing to encrypted columns requires that the extended query protocol
+    (protocol-level prepared statements) be used, so that the values to be
+    encrypted are supplied separately from the SQL command.  For example,
+    using, say, psql or libpq, the following would not work:
+<programlisting>
+-- WRONG!
+INSERT INTO employees (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This would leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of client-side column-level encryption.
+    (And even ignoring that, it could not work because the server does not
+    have access to the keys to perform the encryption.)  Note that using
+    server-side prepared statements using the SQL commands
+    <command>PREPARE</command> and <command>EXECUTE</command> is equally
+    incorrect, since that would also leak the parameters provided to
+    <command>EXECUTE</command> to the server.
+   </para>
+
+   <para>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</programlisting>
+    Higher-level client libraries might use the protocol-level prepared
+    statements automatically and thus won't require any code changes.
+   </para>
+
+   <para>
+    <application>psql</application> provides the command
+    <literal>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   The steps to set up automatic client-side column-level encryption for a
+   database are:
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK, for example, using a cryptographic
+      library or toolkit, or a key management system.  Secure access to the
+      key as appropriate, using access control, passwords, etc.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database using the SQL command <xref
+      linkend="sql-create-column-master-key"/>.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the (unencrypted) key material for the CEK in a temporary
+      location.  (It will be encrypted in the next step.  Depending on the
+      available tools, it might be possible and sensible to combine these two
+      steps.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material using the CMK (created earlier).
+      (The unencrypted version of the CEK key material can now be disposed
+      of.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database using the SQL command <xref
+      linkend="sql-create-column-encryption-key"/>.  This command
+      <quote>uploads</quote> the encrypted CEK key material created in the
+      previous step to the database server.  The local copy of the CEK key
+      material can then be removed.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns using the created CEK.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the client library/driver to be able to look up the CMK
+      created earlier.
+     </para>
+    </listitem>
+   </orderedlist>
+
+   Once this is done, values can be written to and read from the encrypted
+   columns in a transparent way.
+  </para>
+
+  <para>
+   Note that these steps should not be run on the database server, but on some
+   client machine.  Neither the CMK nor the unencrypted CEK should ever appear
+   on the database server host.
+  </para>
+
+  <para>
+   The specific details of this setup depend on the desired CMK storage
+   mechanism/key management system as well as the client libraries to be used.
+   The following example uses the <command>openssl</command> command-line tool
+   to set up the keys.
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK and write it to a file:
+<programlisting>
+openssl genpkey -algorithm rsa -out cmk1.pem
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database:
+<programlisting>
+psql ... -c "CREATE COLUMN MASTER KEY cmk1"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</programlisting>
+      (See <xref linkend="protocol-cek-table"/> for required key lengths.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material:
+<programlisting>
+openssl pkeyutl -encrypt -inkey cmk1.pem -pkeyopt rsa_padding_mode:oaep -in cek1.bin -out cek1.bin.enc
+rm cek1.bin
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database:
+<programlisting>
+# convert file contents to hex encoding; this is just one possible way
+cekenchex=$(perl -0ne 'print unpack "H*"' cek1.bin.enc)
+psql ... -c "CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\x${cekenchex}')"
+rm cek1.bin.enc
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns as shown in the examples above.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the libpq for CMK lookup (see also <xref linkend="libpq-connect-cmklookup"/>):
+<programlisting>
+PGCMKLOOKUP="*=file:$PWD/%k.pem"
+export PGCMKLOOKUP
+</programlisting>
+     </para>
+
+     <para>
+      Additionally, libpq requires that the connection parameter <xref
+      linkend="libpq-connect-column-encryption"/> be set in order to activate
+      the automatic client-side column-level encryption functionality.  This
+      should be done in the connection parameters of the application, but an
+      environment variable (<envar>PGCOLUMNENCRYPTION</envar>) is also
+      available.
+     </para>
+    </listitem>
+   </orderedlist>
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Guidance on Using Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use automatic client-side column-level encryption, and what precautions
+    need to be taken to maintain its security.
+   </para>
+
+   <para>
+    In general, column encryption is never a replacement for additional
+    security and encryption techniques such as transport encryption
+    (SSL/TLS), storage encryption, strong access control, and password
+    security.  Column encryption only targets specific use cases and should be
+    used in conjunction with additional security measures.
+   </para>
+
+   <para>
+    A typical use case for column encryption is to encrypt specific values
+    with additional security requirements, for example credit card numbers.
+    This allows you to store that security-sensitive data together with the
+    rest of your data (thus getting various benefits, such as referential
+    integrity, consistent backups), while giving access to that data only to
+    specific clients and preventing accidental leakage on the server side
+    (server logs, file system backups, etc.).
+   </para>
+
+   <para>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation; for
+    <application>libpq</application>, see <xref
+    linkend="libpq-connect-column-encryption"/>).
+   </para>
+
+   <para>
+    Column encryption cannot hide the existence or absence of data, it can
+    only disguise the particular data that is known to exist.  For example,
+    storing a cleartext person name and an encrypted credit card number
+    indicates that the person has a credit card.  That might not reveal too
+    much if the database is for an online store and there is other data nearby
+    that shows that the person has recently made purchases.  But in another
+    example, storing a cleartext person name and an encrypted diagnosis in a
+    medical database probably indicates that the person has a medical issue.
+    Depending on the circumstances, that might not by itself be sufficient
+    security.
+   </para>
+
+   <para>
+    Encryption cannot completely hide the length of values.  The encryption
+    methods will pad values to multiples of the underlying cipher's block size
+    (usually 16 bytes), so some length differences will be unified this way.
+    There is no concern if all values are of the same length, but if there are
+    signficant length differences between valid values and that length
+    information is security-sensitive, then application-specific workarounds
+    such as padding would need to be applied.  How to do that securely is
+    beyond the scope of this manual.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <tip>
+    <para>
+     One might be inclined to think of the client-side column-level encryption
+     feature as a mechanism for application writers and users to protect
+     themselves against an <quote>evil DBA</quote>, but that is not the
+     intended purpose.  Rather, it is (also) a tool for the DBA to control
+     which data they do not want (in plaintext) on the server.
+    </para>
+   </tip>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on is usually
+     subject to government or industry regulations.  This section is not meant
+     to provide complete instructions on how to do this correctly.  Please
+     seek additional advice when engaging in such projects.
+    </para>
+   </note>
+  </sect2>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
@@ -1986,6 +2420,14 @@ <title>Privileges</title>
        server.  Grantees may also create, alter, or drop their own user
        mappings associated with that server.
       </para>
+      <para>
+       For column master keys, allows the creation of column encryption keys
+       using the master key.
+      </para>
+      <para>
+       For column encryption keys, allows the use of the key in the creation
+       of table columns.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2152,6 +2594,8 @@ <title>ACL Privilege Abbreviations</title>
       <entry><literal>USAGE</literal></entry>
       <entry><literal>U</literal></entry>
       <entry>
+       <literal>COLUMN ENCRYPTION KEY</literal>,
+       <literal>COLUMN MASTER KEY</literal>,
        <literal>DOMAIN</literal>,
        <literal>FOREIGN DATA WRAPPER</literal>,
        <literal>FOREIGN SERVER</literal>,
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 9c6107f960..88e2b30f13 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22859,6 +22859,40 @@ <title>Access Privilege Inquiry Functions</title>
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_encryption_key_privilege</primary>
+        </indexterm>
+        <function>has_column_encryption_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cek</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column encryption key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_master_key_privilege</primary>
+        </indexterm>
+        <function>has_column_master_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cmk</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column master key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
@@ -23349,6 +23383,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 7c01a541fe..818038d860 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,32 @@ <title>Glossary</title>
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-column-encryption-key">
+   <glossterm>Column encryption key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt column values when using automatic
+     client-side column-level encryption (<xref
+     linkend="ddl-column-encryption"/>).  Column encryption keys are stored in
+     the database encrypted by another key, the <glossterm
+     linkend="glossary-column-master-key">column master key</glossterm>.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt <glossterm
+     linkend="glossary-column-encryption-key">column encryption
+     keys</glossterm>.  (So the column master key is a <firstterm>key
+     encryption key</firstterm>.)  Column master keys are stored outside the
+     database system, for example in a key management system.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-commit">
    <glossterm>Commit</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3ccd8ff942..26cab10104 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,141 @@ <title>Parameter Key Words</title>
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-connect-column-encryption" xreflabel="column_encryption">
+      <term><literal>column_encryption</literal></term>
+      <listitem>
+       <para>
+        If set to <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, this enables automatic client-side column-level
+        encryption for the connection.  If encrypted columns are queried and
+        this is not enabled, the encrypted values are returned.  See <xref
+        linkend="ddl-column-encryption"/> for more information about this
+        feature.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-cmklookup" xreflabel="cmklookup">
+      <term><literal>cmklookup</literal></term>
+      <listitem>
+       <para>
+        This specifies how libpq should look up column master keys (CMKs) in
+        order to decrypt the column encryption keys (CEKs).
+        The value is a list of <literal>key=value</literal> entries separated
+        by semicolons.  Each key is the name of a key realm, or
+        <literal>*</literal> to match all realms.  The value is a
+        <literal>scheme:data</literal> specification.  The scheme specifies
+        the method to look up the key, the remaining data is specific to the
+        scheme.  Placeholders are replaced in the remaining data as follows:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>%a</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name (see <xref linkend="protocol-cmk-table"/>)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%r</literal></term>
+          <listitem>
+           <para>
+            The realm name
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        Available schemes are:
+        <variablelist>
+         <varlistentry>
+          <term><literal>file</literal></term>
+          <listitem>
+           <para>
+            Load the key material from a file.  The remaining data is the file
+            name.  Use this if the CMKs are kept in a file on the file system.
+           </para>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>run</literal></term>
+          <listitem>
+           <para>
+            Run the specified command to decrypt the CEK.  The remaining data
+            is a shell command.  Use this with key management systems that
+            perform the decryption themselves.  The command must print the
+            decrypted plaintext on the standard output.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        The default value is empty.
+       </para>
+
+       <para>
+        Example:
+<programlisting>
+cmklookup="r1=file:/some/where/secrets/%k.pem;*=file:/else/where/%r/%k.pem"
+</programlisting>
+        This specification says, for keys in realm <quote>r1</quote>, load
+        them from the specified file, replacing <literal>%k</literal> by the
+        key name.  For keys in other realms, load them from the file,
+        replacing realm and key names as specified.
+       </para>
+
+       <para>
+        An example for interacting with a (hypothetical) key management
+        system:
+<programlisting>
+cmklookup="*=run:acmekms decrypt --key %k --alg %a --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2999,32 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            half-byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this half-byte to one to force
+            encryption.  For example, use the C code literal
+            <literal>0x10</literal> to specify text format with forced
+            encryption.  If the array pointer is null then encryption is not
+            forced for any parameter.
+           </para>
+
+           <para>
+            Parameters corresponding to encrypted columns must be passed in
+            text format.  Specifying binary format for such a parameter will
+            result in an error.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter does not
+            correspond to an encrypted column on the server, then the call
+            will fail and the parameter will not be sent.  This can be used
+            for additional security against a compromised server.  (The
+            drawback is that application code then needs to be kept up to date
+            with knowledge about which columns are encrypted rather than
+            letting the server specify this.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3037,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3196,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Sends a request to execute a prepared statement with given
+        parameters, and waits for the result, with support for encrypted columns.
+<synopsis>
+PGresult *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> is like <xref
+        linkend="libpq-PQexecPrepared"/> with additional support for encrypted
+        columns.  The parameter <parameter>paramDesc</parameter> must be a
+        result set obtained from <xref linkend="libpq-PQdescribePrepared"/> on
+        the same prepared statement.
+       </para>
+
+       <para>
+        This function must be used if a statement parameter corresponds to an
+        underlying encrypted column.  In that situation, the prepared
+        statement needs to be described first so that libpq can obtain the
+        necessary key and other information from the server.  When that is
+        done, the parameters corresponding to encrypted columns are
+        automatically encrypted appropriately before being sent to the server.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4084,28 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQfisencrypted">
+     <term><function>PQfisencrypted</function><indexterm><primary>PQfisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given column came from an encrypted
+       column.  Column numbers start at 0.
+<synopsis>
+int PQfisencrypted(const PGresult *res,
+                   int column_number);
+</synopsis>
+      </para>
+
+      <para>
+       Encrypted column values are automatically decrypted, so this function
+       is not necessary to access the column value.  It can be used for extra
+       security to check whether the value was stored encrypted when one
+       thought it should be.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQfsize">
      <term><function>PQfsize</function><indexterm><primary>PQfsize</primary></indexterm></term>
 
@@ -4059,6 +4287,31 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQparamisencrypted">
+     <term><function>PQparamisencrypted</function><indexterm><primary>PQparamisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given parameter is destined for an
+       encrypted column.  Parameter numbers start at 0.
+<synopsis>
+int PQparamisencrypted(const PGresult *res, int param_number);
+</synopsis>
+      </para>
+
+      <para>
+       Values for parameters destined for encrypted columns are automatically
+       encrypted, so this function is not necessary to prepare the parameter
+       value.  It can be used for extra security to check whether the value
+       will be stored encrypted when one thought it should be.  (But see also
+       at <xref linkend="libpq-PQexecPreparedDescribed"/> for another way to do that.)
+       This function is only useful when inspecting the result of <xref
+       linkend="libpq-PQdescribePrepared"/>.  For other types of results it
+       will return false.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQprint">
      <term><function>PQprint</function><indexterm><primary>PQprint</primary></indexterm></term>
 
@@ -4584,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4845,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4902,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4963,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Sends a request to execute a prepared statement with given
+       parameters, without waiting for the result(s), with support for encrypted columns.
+<synopsis>
+int PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> is like <xref
+       linkend="libpq-PQsendQueryPrepared"/> with additional support for encrypted
+       columns.  The parameter <parameter>paramDesc</parameter> must be a
+       result set obtained from <xref linkend="libpq-PQsendDescribePrepared"/> on
+       the same prepared statement.
+      </para>
+
+      <para>
+       This function must be used if a statement parameter corresponds to an
+       underlying encrypted column.  In that situation, the prepared
+       statement needs to be described first so that libpq can obtain the
+       necessary key and other information from the server.  When that is
+       done, the parameters corresponding to encrypted columns are
+       automatically encrypted appropriately before being sent to the server.
+       See also under <xref linkend="libpq-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5052,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8086,26 @@ <title>Environment Variables</title>
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCMKLOOKUP</envar></primary>
+      </indexterm>
+      <envar>PGCMKLOOKUP</envar> behaves the same as the <xref
+      linkend="libpq-connect-cmklookup"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCOLUMNENCRYPTION</envar></primary>
+      </indexterm>
+      <envar>PGCOLUMNENCRYPTION</envar> behaves the same as the <xref
+      linkend="libpq-connect-column-encryption"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 73b7f4432f..c43c3051c3 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,76 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-column-encryption">
+   <title>Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    Automatic client-side column-level encryption is enabled by sending the
+    parameter <literal>_pq_.column_encryption</literal> with a value of
+    <literal>1</literal> in the StartupMessage.  This is a protocol extension
+    that enables a few additional protocol messages and adds additional fields
+    to existing protocol messages.  Client drivers should only activate this
+    protocol extension when requested by the user, for example through a
+    connection parameter.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the
+    messages ColumnMasterKey and ColumnEncryptionKey can appear before
+    RowDescription and ParameterDescription messages.  Clients should collect
+    the information in these messages and keep them for the duration of the
+    connection.  A server is not required to resend the key information for
+    each statement cycle if it was already sent during this connection.  If a
+    server resends a key that the client has already stored (that is, a key
+    having an ID equal to one already stored), the new information should
+    replace the old.  (This could happen, for example, if the key was altered
+    by server-side DDL commands.)
+   </para>
+
+   <para>
+    A client supporting automatic column-level encryption should automatically
+    decrypt the column value fields of DataRow messages corresponding to
+    encrypted columns, and it should automatically encrypt the parameter value
+    fields of Bind messages corresponding to encrypted columns.
+   </para>
+
+   <para>
+    When column encryption is used, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column-level encryption protocol option is enabled.  That way, a client
+    library only needs to support decrypting data sent in binary and does not
+    have to support decoding the text format of the encryption-related types
+    (see <xref linkend="datatype-encrypted"/>).
+   </para>
+
+   <para>
+    When deterministic encryption is used, clients need to take care to
+    represent plaintext to be encrypted in a consistent form.  For example,
+    encrypting an integer represented by the string <literal>100</literal> and
+    an integer represented by the string <literal>+100</literal> would result
+    in two different ciphertexts, thus defeating the main point of
+    deterministic encryption.  This protocol specification requires the
+    plaintext to be in <quote>canonical</quote> form, which is the form that
+    is produced by the server when it outputs a particular value in text
+    format.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the client
+    encoding must match the server encoding.  This ensures that all values
+    encrypted or decrypted by the client match the server encoding.
+   </para>
+
+   <para>
+    The cryptographic operations used for automatic client-side column-level
+    encryption are described in <xref
+    linkend="protocol-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -3841,6 +3911,16 @@ <title>Message Formats</title>
          The parameter format codes.  Each must presently be
          zero (text) or one (binary).
         </para>
+
+        <para>
+         If the protocol extension <literal>_pq_.column_encryption</literal>
+         is enabled (see <xref linkend="protocol-flow-column-encryption"/>),
+         then the second-least-significant half-byte is set to one if the
+         parameter was encrypted by the client.  (So, for example, to send an
+         encrypted value in binary, the field is set to 0x11 in total.)  This
+         is used by the server to check that a parameter that was required to
+         be encrypted was actually encrypted.
+        </para>
        </listitem>
       </varlistentry>
 
@@ -4061,6 +4141,140 @@ <title>Message Formats</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ColumnEncryptionKey">
+    <term>ColumnEncryptionKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('Y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column encryption key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the master key used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the algorithm used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The length of the following key material.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Byte<replaceable>n</replaceable></term>
+       <listitem>
+        <para>
+         The key material, encrypted with the master key referenced above.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="protocol-message-formats-ColumnMasterKey">
+    <term>ColumnMasterKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column master key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The key's realm.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="protocol-message-formats-CommandComplete">
     <term>CommandComplete (B)</term>
     <listitem>
@@ -5164,6 +5378,45 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each parameter:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         This is used as a bit field of flags.  If the parameter is to be
+         encrypted and bit 0x0001 is set, the column underlying the parameter
+         uses deterministic encryption, otherwise randomized encryption.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5552,6 +5805,50 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each field:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         This is used as a bit field of flags.  If the field is encrypted and
+         bit 0x0001 is set, the field uses deterministic encryption, otherwise
+         randomized encryption.
+        </para>
+        <!--
+            This is not really useful here, but it keeps alignment with
+            ParameterDescription.  Future flags might be useful in both
+            places.
+        -->
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7377,6 +7674,176 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-column-encryption-crypto">
+  <title>Automatic Client-side Column-level Encryption Cryptography</title>
+
+  <para>
+   This section describes the cryptographic operations used by the automatic
+   client-side column-level encryption functionality.  A client that supports
+   this functionality needs to implement these operations as specified here in
+   order to be able to interoperate with other clients.
+  </para>
+
+  <para>
+   Column encryption key algorithms and column master key algorithms are
+   identified by integers in the protocol messages and the system catalogs.
+   Additional algorithms may be added to this protocol specification without a
+   change in the protocol version number.  Clients should implement support
+   for all the algorithms specified here.  If a client encounters an algorithm
+   identifier it does not recognize or does not support, it must raise an
+   error.  A suitable error message should be provided to the application or
+   user.
+  </para>
+
+  <sect2 id="protocol-cmk">
+   <title>Column Master Keys</title>
+
+   <para>
+    The currently defined algorithms for column master keys are listed in
+    <xref linkend="protocol-cmk-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC
+       8017</ulink>/PKCS #1)</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect2>
+
+  <sect2 id="protocol-cek">
+   <title>Column Encryption Keys</title>
+
+   <para>
+    The currently defined algorithms for column encryption keys are listed in
+    <xref linkend="protocol-cek-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</entry>
+       <entry><literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</entry>
+       <entry><literal>AEAD_AES_192_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_512</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <para>
+    The <quote>associated data</quote> in these algorithms consists of 4
+    bytes: The ASCII letters <literal>P</literal> and <literal>G</literal>
+    (byte values 80 and 71), followed by the version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </para>
+
+   <para>
+    The length of the initialization vector is 16 octets for all CEK algorithm
+    variants.  For randomized encryption, the initialization vector should be
+    (cryptographically strong) random bytes.  For deterministic encryption,
+    the initialization vector is constructed as
+<programlisting>
+SUBSTRING(<replaceable>HMAC</replaceable>(<replaceable>K</replaceable>, <replaceable>P</replaceable>) FOR <replaceable>IVLEN</replaceable>)
+</programlisting>
+    where <replaceable>HMAC</replaceable> is the HMAC function associated with
+    the algorithm, <replaceable>K</replaceable> is the IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </para>
+  </sect2>
+ </sect1>
+
  <sect1 id="protocol-changes">
   <title>Summary of Changes since Protocol 2.0</title>
 
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 54b5f22d6e..a730e5d650 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -8,6 +8,8 @@
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
+<!ENTITY alterColumnEncryptionKey SYSTEM "alter_column_encryption_key.sgml">
+<!ENTITY alterColumnMasterKey SYSTEM "alter_column_master_key.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
 <!ENTITY alterDefaultPrivileges SYSTEM "alter_default_privileges.sgml">
@@ -62,6 +64,8 @@
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
+<!ENTITY createColumnEncryptionKey SYSTEM "create_column_encryption_key.sgml">
+<!ENTITY createColumnMasterKey SYSTEM "create_column_master_key.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
 <!ENTITY createDatabase     SYSTEM "create_database.sgml">
 <!ENTITY createDomain       SYSTEM "create_domain.sgml">
@@ -109,6 +113,8 @@
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
+<!ENTITY dropColumnEncryptionKey SYSTEM "drop_column_encryption_key.sgml">
+<!ENTITY dropColumnMasterKey SYSTEM "drop_column_master_key.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
 <!ENTITY dropDatabase       SYSTEM "drop_database.sgml">
 <!ENTITY dropDomain         SYSTEM "drop_domain.sgml">
diff --git a/doc/src/sgml/ref/alter_column_encryption_key.sgml b/doc/src/sgml/ref/alter_column_encryption_key.sgml
new file mode 100644
index 0000000000..655e1e00d8
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+doc/src/sgml/ref/alter_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-encryption-key">
+ <indexterm zone="sql-alter-column-encryption-key">
+  <primary>ALTER COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>change the definition of a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> ADD VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> DROP VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN ENCRYPTION KEY</command> changes the definition of a
+   column encryption key.
+  </para>
+
+  <para>
+   The first form adds new encrypted key data to a column encryption key,
+   which must be encrypted with a different column master key than the
+   existing key data.  The second form removes a key data entry for a given
+   column master key.  Together, these forms can be used for column master key
+   rotation.
+  </para>
+
+  <para>
+   You must own the column encryption key to use <command>ALTER COLUMN
+   ENCRYPTION KEY</command>.  To alter the owner, you must also be a direct or
+   indirect member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column encryption key's
+   schema.  (These restrictions enforce that altering the owner doesn't do
+   anything you couldn't do by dropping and recreating the column encryption
+   key.  However, a superuser can alter ownership of any column encryption key
+   anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column encryption
+      key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  See <xref
+      linkend="sql-create-column-encryption-key"/> for details
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rotate the master keys used to encrypt a given column encryption key,
+   use a command sequence like this:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (
+    COLUMN_MASTER_KEY = cmk1
+);
+</programlisting>
+  </para>
+
+  <para>
+   To rename the column encryption key <literal>cek1</literal> to
+   <literal>cek2</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column encryption key <literal>cek1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN ENCRYPTION KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/alter_column_master_key.sgml b/doc/src/sgml/ref/alter_column_master_key.sgml
new file mode 100644
index 0000000000..7f0e656ef0
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+doc/src/sgml/ref/alter_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-master-key">
+ <indexterm zone="sql-alter-column-master-key">
+  <primary>ALTER COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN MASTER KEY</refname>
+  <refpurpose>change the definition of a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> ( REALM = <replaceable>realm</replaceable> )
+
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </para>
+
+  <para>
+   You must own the column master key to use <command>ALTER COLUMN MASTER
+   KEY</command>.  To alter the owner, you must also be a direct or indirect
+   member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column master key's schema.
+   (These restrictions enforce that altering the owner doesn't do anything you
+   couldn't do by dropping and recreating the column master key.  However, a
+   superuser can alter ownership of any column master key anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the column master key <literal>cmk1</literal> to
+   <literal>cmk2</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column master key <literal>cmk1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN MASTER KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..1caf9bfa56 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..ebcbf5d00a 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,16 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support automatic client-side
+    column-level encryption or decryption; its input or output data will
+    always be the ciphertext.  This is usually suitable for backups (see also
+    <xref linkend="app-pgdump"/>).  If automatic client-side encryption or
+    decryption is wanted, <command>INSERT</command> and
+    <command>SELECT</command> need to be used instead to write and read the
+    data.
+   </para>
+
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/ref/create_column_encryption_key.sgml b/doc/src/sgml/ref/create_column_encryption_key.sgml
new file mode 100644
index 0000000000..65534fb03f
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,173 @@
+<!--
+doc/src/sgml/ref/create_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-encryption-key">
+ <indexterm zone="sql-create-column-encryption-key">
+  <primary>CREATE COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>define a new column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN ENCRYPTION KEY <replaceable>name</replaceable> WITH VALUES (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    ALGORITHM = <replaceable>algorithm</replaceable>,
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+[ , ... ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN ENCRYPTION KEY</command> defines a new column
+   encryption key.  A column encryption key is used for client-side encryption
+   of table columns that have been defined as encrypted.  The key material of
+   a column encryption key is stored in the database's system catalogs,
+   encrypted (wrapped) by a column master key (which in turn is only
+   accessible to the client, not the database server).
+  </para>
+
+  <para>
+   A column encryption key can be associated with more than one column master
+   key.  To specify that, specify more than one parenthesized definition (see
+   also the examples).
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column encryption key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.  You must have <literal>USAGE</literal> privilege on the
+      column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  Supported algorithms are:
+      <itemizedlist>
+       <listitem>
+        <para><literal>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></para>
+       </listitem>
+      </itemizedlist>
+     </para>
+
+     <para>
+      This is informational only.  The specified value is provided to the
+      client, which may use it for decrypting the column encryption key on the
+      client side.  But a client is also free to ignore this information and
+      figure out how to arrange the decryption in some other way.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+</programlisting>
+  </para>
+
+  <para>
+   To specify more than one associated column master key:
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\xF1F2F2F4...'
+);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_column_master_key.sgml b/doc/src/sgml/ref/create_column_master_key.sgml
new file mode 100644
index 0000000000..6aaa1088d1
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+doc/src/sgml/ref/create_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-master-key">
+ <indexterm zone="sql-create-column-master-key">
+  <primary>CREATE COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN MASTER KEY</refname>
+  <refpurpose>define a new column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN MASTER KEY <replaceable>name</replaceable> [ WITH (
+    [ REALM = <replaceable>realm</replaceable> ]
+) ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN MASTER KEY</command> defines a new column master
+   key.  A column master key is used to encrypt column encryption keys, which
+   are the keys that actually encrypt the column data.  The key material of
+   the column master key is not stored in the database.  The definition of a
+   column master key records information that will allow a client to locate
+   the key material, for example in a file or in a key management system.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column master key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>realm</replaceable></term>
+
+    <listitem>
+     <para>
+      This is an optional string that can be used to organize column master
+      keys into groups for lookup by clients.  The intent is that all column
+      master keys that are stored in the same system (file system location,
+      key management system, etc.) should be in the same realm.  A client
+      would then be configured to look up all keys in a given realm in a
+      certain way.  See the documentation of the respective client library for
+      further usage instructions.
+     </para>
+
+     <para>
+      The default is the empty string.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+<programlisting>
+CREATE COLUMN MASTER KEY cmk1 (realm = 'myrealm');
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index a03dee4afe..d1549c7f45 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -22,7 +22,7 @@
  <refsynopsisdiv>
 <synopsis>
 CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable> ( [
-  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,47 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables automatic client-side column-level encryption for the column.
+      <replaceable>encryption_options</replaceable> are comma-separated
+      <literal>key=value</literal> specifications.  The following options are
+      available:
+      <variablelist>
+       <varlistentry>
+        <term><literal>column_encryption_key</literal></term>
+        <listitem>
+         <para>
+          Specifies the name of the column encryption key to use.  Specifying
+          this is mandatory.  You must have <literal>USAGE</literal> privilege
+          on the column encryption key.
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>encryption_type</literal></term>
+        <listitem>
+         <para>
+          <literal>randomized</literal> (the default) or <literal>deterministic</literal>
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>algorithm</literal></term>
+        <listitem>
+         <para>
+          The encryption algorithm to use.  The default is
+          <literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -704,6 +745,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</literal></term>
         <listitem>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523ca..6a94706ef7 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@
 
  <refsynopsisdiv>
 <synopsis>
-DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
+DISCARD { ALL | COLUMN ENCRYPTION KEYS | PLANS | SEQUENCES | TEMPORARY | TEMP }
 </synopsis>
  </refsynopsisdiv>
 
@@ -42,6 +42,17 @@ <title>Parameters</title>
 
   <variablelist>
 
+   <varlistentry>
+    <term><literal>COLUMN ENCRYPTION KEYS</literal></term>
+    <listitem>
+     <para>
+      Discards knowledge about which column encryption keys and column master
+      keys have been sent to the client in this session.  (They will
+      subsequently be re-sent as required.)
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>PLANS</literal></term>
     <listitem>
@@ -93,6 +104,7 @@ <title>Parameters</title>
 DISCARD PLANS;
 DISCARD TEMP;
 DISCARD SEQUENCES;
+DISCARD COLUMN ENCRYPTION KEYS;
 </programlisting></para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/ref/drop_column_encryption_key.sgml b/doc/src/sgml/ref/drop_column_encryption_key.sgml
new file mode 100644
index 0000000000..f2ac1beb08
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_encryption_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-encryption-key">
+ <indexterm zone="sql-drop-column-encryption-key">
+  <primary>DROP COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>remove a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN ENCRYPTION KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN ENCRYPTION KEY</command> removes a previously defined
+   column encryption key.  To be able to drop a column encryption key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column encryption key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column encryption key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column encryption key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column encryption key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN ENCRYPTION KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_column_master_key.sgml b/doc/src/sgml/ref/drop_column_master_key.sgml
new file mode 100644
index 0000000000..fae95e09d1
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_master_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-master-key">
+ <indexterm zone="sql-drop-column-master-key">
+  <primary>DROP COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN MASTER KEY</refname>
+  <refpurpose>remove a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN MASTER KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN MASTER KEY</command> removes a previously defined
+   column master key.  To be able to drop a column master key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column master key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column master key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column master key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column master key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN MASTER KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 35bf0332c8..f712f8e9e4 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -46,6 +46,16 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN ENCRYPTION KEY <replaceable>cek_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN MASTER KEY <replaceable>cmk_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON DOMAIN <replaceable>domain_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -513,7 +523,7 @@ <title>Compatibility</title>
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
+    Privileges on databases, tablespaces, schemas, keys, languages, and
     configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 334e4b7fd1..5412ea8666 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -716,6 +716,48 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option turns on the column encryption connection option in
+        <application>libpq</application> (see <xref
+        linkend="libpq-connect-column-encryption"/>).  Column master key
+        lookup must be configured by the user, either through a connection
+        option or an environment setting (see <xref
+        linkend="libpq-connect-cmklookup"/>).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  (But then it is recommended
+        to not do this on the same host as the server, to avoid exposing
+        unencrypted data that is meant to be kept encrypted on the server.)
+        Note that a dump created with this option cannot be restored into a
+        database with column encryption.
+       </para>
+       <!--
+           XXX: The latter would require another pg_dump option to put out
+           INSERT ... \bind ... \g commands.
+       -->
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index e62d05e5ab..4bf60c729f 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -259,6 +259,33 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  Note that a dump created
+        with this option cannot be restored into a database with column
+        encryption.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7b8ae9fac3..b6115e433e 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4029,6 +4057,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <term><varname>HIDE_COLUMN_ENCRYPTION</varname></term>
+        <listitem>
+        <para>
+         If this variable is set to <literal>true</literal>, column encryption
+         details are not displayed. This is mainly useful for regression
+         tests.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..c898997915 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -36,6 +36,8 @@ <title>SQL Commands</title>
    &abort;
    &alterAggregate;
    &alterCollation;
+   &alterColumnEncryptionKey;
+   &alterColumnMasterKey;
    &alterConversion;
    &alterDatabase;
    &alterDefaultPrivileges;
@@ -90,6 +92,8 @@ <title>SQL Commands</title>
    &createAggregate;
    &createCast;
    &createCollation;
+   &createColumnEncryptionKey;
+   &createColumnMasterKey;
    &createConversion;
    &createDatabase;
    &createDomain;
@@ -137,6 +141,8 @@ <title>SQL Commands</title>
    &dropAggregate;
    &dropCast;
    &dropCollation;
+   &dropColumnEncryptionKey;
+   &dropColumnMasterKey;
    &dropConversion;
    &dropDatabase;
    &dropDomain;
diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index ef818228ac..c5894893eb 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -20,7 +20,9 @@
 
 #include "access/printsimple.h"
 #include "catalog/pg_type.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 
 /*
@@ -46,6 +48,12 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc)
 		pq_sendint16(&buf, attr->attlen);
 		pq_sendint32(&buf, attr->atttypmod);
 		pq_sendint16(&buf, 0);	/* format code */
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&buf, 0);	/* CEK */
+			pq_sendint32(&buf, 0);	/* CEK alg */
+			pq_sendint16(&buf, 0);	/* flags */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 72faeb5dfa..2d627b6f47 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,28 @@
  */
 #include "postgres.h"
 
+#include "access/genam.h"
 #include "access/printtup.h"
+#include "access/skey.h"
+#include "access/table.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
 #include "libpq/libpq.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memdebug.h"
 #include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
 
 
 static void printtup_startup(DestReceiver *self, int operation,
@@ -151,6 +166,166 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 	 */
 }
 
+/*
+ * Send ColumnMasterKey message, unless it's already been sent in this session
+ * for this key.
+ */
+List	   *cmk_sent = NIL;
+
+static void
+cmk_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+}
+
+static void
+MaybeSendColumnMasterKeyMessage(Oid cmkid)
+{
+	HeapTuple	tuple;
+	Form_pg_colmasterkey cmkform;
+	Datum		datum;
+	bool		isnull;
+	StringInfoData buf;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cmk_sent, cmkid))
+		return;
+
+	tuple = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	cmkform = (Form_pg_colmasterkey) GETSTRUCT(tuple);
+
+	pq_beginmessage(&buf, 'y'); /* ColumnMasterKey */
+	pq_sendint32(&buf, cmkform->oid);
+	pq_sendstring(&buf, NameStr(cmkform->cmkname));
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkrealm, &isnull);
+	Assert(!isnull);
+	pq_sendstring(&buf, TextDatumGetCString(datum));
+	pq_endmessage(&buf);
+
+	ReleaseSysCache(tuple);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CMKOID, cmk_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cmk_sent = lappend_oid(cmk_sent, cmkid);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * Send ColumnEncryptionKey message, unless it's already been sent in this
+ * session for this key.
+ */
+List	   *cek_sent = NIL;
+
+static void
+cek_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
+void
+MaybeSendColumnEncryptionKeyMessage(Oid attcek)
+{
+	HeapTuple	tuple;
+	ScanKeyData skey[1];
+	SysScanDesc sd;
+	Relation	rel;
+	bool		found = false;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cek_sent, attcek))
+		return;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	ScanKeyInit(&skey[0],
+				Anum_pg_colenckeydata_ckdcekid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(attcek));
+	rel = table_open(ColumnEncKeyDataRelationId, AccessShareLock);
+	sd = systable_beginscan(rel, ColumnEncKeyCekidCmkidIndexId, true, NULL, 1, skey);
+
+	while ((tuple = systable_getnext(sd)))
+	{
+		Form_pg_colenckeydata ckdform = (Form_pg_colenckeydata) GETSTRUCT(tuple);
+		Datum		datum;
+		bool		isnull;
+		bytea	   *ba;
+		StringInfoData buf;
+
+		MaybeSendColumnMasterKeyMessage(ckdform->ckdcmkid);
+
+		datum = heap_getattr(tuple, Anum_pg_colenckeydata_ckdencval, RelationGetDescr(rel), &isnull);
+		Assert(!isnull);
+		ba = pg_detoast_datum_packed((bytea *) DatumGetPointer(datum));
+
+		pq_beginmessage(&buf, 'Y'); /* ColumnEncryptionKey */
+		pq_sendint32(&buf, ckdform->ckdcekid);
+		pq_sendint32(&buf, ckdform->ckdcmkid);
+		pq_sendint32(&buf, ckdform->ckdcmkalg);
+		pq_sendint32(&buf, VARSIZE_ANY_EXHDR(ba));
+		pq_sendbytes(&buf, VARDATA_ANY(ba), VARSIZE_ANY_EXHDR(ba));
+		pq_endmessage(&buf);
+
+		found = true;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	systable_endscan(sd);
+	table_close(rel, NoLock);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CEKDATAOID, cek_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cek_sent = lappend_oid(cek_sent, attcek);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+void
+DiscardColumnEncryptionKeys(void)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -167,6 +342,7 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 						  List *targetlist, int16 *formats)
 {
 	int			natts = typeinfo->natts;
+	size_t		sz;
 	int			i;
 	ListCell   *tlist_item = list_head(targetlist);
 
@@ -183,14 +359,18 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 	 * Have to overestimate the size of the column-names, to account for
 	 * character set overhead.
 	 */
-	enlargeStringInfo(buf, (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
-							+ sizeof(Oid)	/* resorigtbl */
-							+ sizeof(AttrNumber)	/* resorigcol */
-							+ sizeof(Oid)	/* atttypid */
-							+ sizeof(int16) /* attlen */
-							+ sizeof(int32) /* attypmod */
-							+ sizeof(int16) /* format */
-							) * natts);
+	sz = (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
+		  + sizeof(Oid)	/* resorigtbl */
+		  + sizeof(AttrNumber)	/* resorigcol */
+		  + sizeof(Oid)	/* atttypid */
+		  + sizeof(int16) /* attlen */
+		  + sizeof(int32) /* attypmod */
+		  + sizeof(int16)); /* format */
+	if (MyProcPort->column_encryption_enabled)
+		sz += (sizeof(int32)		/* attcekid */
+			   + sizeof(int32)		/* attencalg */
+			   + sizeof(int16));	/* flags */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +380,9 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
+		int16		flags = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +414,31 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(atttypid))
+		{
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(resorigtbl), Int16GetDatum(resorigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", resorigcol, resorigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			atttypid = orig_att->attusertypid;
+			atttypmod = orig_att->attusertypmod;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->atttypmod;
+			if (orig_att->atttypid == PG_ENCRYPTED_DETOID)
+				flags |= 0x0001;
+			ReleaseSysCache(tp);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +446,12 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		pq_writeint16(buf, att->attlen);
 		pq_writeint32(buf, atttypmod);
 		pq_writeint16(buf, format);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_writeint32(buf, attcekid);
+			pq_writeint32(buf, attencalg);
+			pq_writeint16(buf, flags);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +485,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 72a2c3d3db..f86ba299c3 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attusertypid != attr2->attusertypid)
+			return false;
+		if (attr1->attusertypmod != attr2->attusertypmod)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 24bab58499..cc48069932 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index a60107bf94..7b9575635b 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index c4232344aa..a3547c6cae 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_default_acl.h"
@@ -247,6 +249,12 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_SEQUENCE:
 			whole_mask = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			whole_mask = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			whole_mask = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			whole_mask = ACL_ALL_RIGHTS_DATABASE;
 			break;
@@ -473,6 +481,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
 			errormsg = gettext_noop("invalid privilege type %s for sequence");
 			break;
+		case OBJECT_CEK:
+			all_privileges = ACL_ALL_RIGHTS_CEK;
+			errormsg = gettext_noop("invalid privilege type %s for column encryption key");
+			break;
+		case OBJECT_CMK:
+			all_privileges = ACL_ALL_RIGHTS_CMK;
+			errormsg = gettext_noop("invalid privilege type %s for column master key");
+			break;
 		case OBJECT_DATABASE:
 			all_privileges = ACL_ALL_RIGHTS_DATABASE;
 			errormsg = gettext_noop("invalid privilege type %s for database");
@@ -597,6 +613,12 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_SEQUENCE:
 			ExecGrant_Relation(istmt);
 			break;
+		case OBJECT_CEK:
+			ExecGrant_common(istmt, ColumnEncKeyRelationId, ACL_ALL_RIGHTS_CEK, NULL);
+			break;
+		case OBJECT_CMK:
+			ExecGrant_common(istmt, ColumnMasterKeyRelationId, ACL_ALL_RIGHTS_CMK, NULL);
+			break;
 		case OBJECT_DATABASE:
 			ExecGrant_common(istmt, DatabaseRelationId, ACL_ALL_RIGHTS_DATABASE, NULL);
 			break;
@@ -676,6 +698,26 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 				objects = lappend_oid(objects, relOid);
 			}
 			break;
+		case OBJECT_CEK:
+			foreach(cell, objnames)
+			{
+				List	   *cekname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cek_oid(cekname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
+		case OBJECT_CMK:
+			foreach(cell, objnames)
+			{
+				List	   *cmkname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cmk_oid(cmkname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
 		case OBJECT_DATABASE:
 			foreach(cell, objnames)
 			{
@@ -2693,6 +2735,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("permission denied for column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("permission denied for column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -2798,6 +2846,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -2828,6 +2877,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("must be owner of aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("must be owner of column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("must be owner of column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("must be owner of collation %s");
 						break;
@@ -2938,6 +2993,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3019,6 +3075,10 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
+		case OBJECT_CEK:
+			return object_aclmask(ColumnEncKeyRelationId, object_oid, roleid, mask, how);
+		case OBJECT_CMK:
+			return object_aclmask(ColumnMasterKeyRelationId, object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
 		case OBJECT_FUNCTION:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..cab6cfd140 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -30,7 +30,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -153,6 +156,9 @@ static const Oid object_classes[] = {
 	TypeRelationId,				/* OCLASS_TYPE */
 	CastRelationId,				/* OCLASS_CAST */
 	CollationRelationId,		/* OCLASS_COLLATION */
+	ColumnEncKeyRelationId,		/* OCLASS_CEK */
+	ColumnEncKeyDataRelationId,	/* OCLASS_CEKDATA */
+	ColumnMasterKeyRelationId,	/* OCLASS_CMK */
 	ConstraintRelationId,		/* OCLASS_CONSTRAINT */
 	ConversionRelationId,		/* OCLASS_CONVERSION */
 	AttrDefaultRelationId,		/* OCLASS_DEFAULT */
@@ -1493,6 +1499,9 @@ doDeletion(const ObjectAddress *object, int flags)
 
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONVERSION:
 		case OCLASS_LANGUAGE:
 		case OCLASS_OPCLASS:
@@ -2859,6 +2868,15 @@ getObjectClass(const ObjectAddress *object)
 		case CollationRelationId:
 			return OCLASS_COLLATION;
 
+		case ColumnEncKeyRelationId:
+			return OCLASS_CEK;
+
+		case ColumnEncKeyDataRelationId:
+			return OCLASS_CEKDATA;
+
+		case ColumnMasterKeyRelationId:
+			return OCLASS_CMK;
+
 		case ConstraintRelationId:
 			return OCLASS_CONSTRAINT;
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 4f006820b8..df282c796f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypid - 1] = ObjectIdGetDatum(attrs->attusertypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypmod - 1] = Int32GetDatum(attrs->attusertypmod);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attusertypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attusertypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 14e57adee2..00f914bc5f 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -26,6 +26,8 @@
 #include "catalog/dependency.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1997,6 +1999,254 @@ OpfamilyIsVisible(Oid opfid)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4567,6 +4817,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(OpfamilyIsVisible(oid));
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2f688166e1..6c4ab9ac59 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -191,6 +194,48 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_COLLATION,
 		true
 	},
+	{
+		"column encryption key",
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		CEKOID,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		Anum_pg_colenckey_cekowner,
+		Anum_pg_colenckey_cekacl,
+		OBJECT_CEK,
+		true
+	},
+	{
+		"column encryption key data",
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		CEKDATAOID,
+		-1,
+		Anum_pg_colenckeydata_oid,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		-1,
+		false
+	},
+	{
+		"column master key",
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		CMKOID,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		Anum_pg_colmasterkey_cmkowner,
+		Anum_pg_colmasterkey_cmkacl,
+		OBJECT_CMK,
+		true
+	},
 	{
 		"constraint",
 		ConstraintRelationId,
@@ -723,6 +768,18 @@ static const struct object_type_map
 	{
 		"collation", OBJECT_COLLATION
 	},
+	/* OCLASS_CEK */
+	{
+		"column encryption key", OBJECT_CEK
+	},
+	/* OCLASS_CEKDATA */
+	{
+		"column encryption key data", OBJECT_CEKDATA
+	},
+	/* OCLASS_CMK */
+	{
+		"column master key", OBJECT_CMK
+	},
 	/* OCLASS_CONSTRAINT */
 	{
 		"table constraint", OBJECT_TABCONSTRAINT
@@ -1029,6 +1086,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1108,6 +1175,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, castNode(List, object));
+					Oid			cekid;
+					Oid			cmkid;
+
+					cekid = get_cek_oid(cekname, missing_ok);
+					cmkid = get_cmk_oid(cmkname, missing_ok);
+
+					address.classId = ColumnEncKeyDataRelationId;
+					address.objectId = get_cekdata_oid(cekid, cmkid, missing_ok);
+					address.objectSubId = 0;
+				}
+				break;
 			case OBJECT_TRANSFORM:
 				{
 					TypeName   *typename = linitial_node(TypeName, castNode(List, object));
@@ -2311,6 +2393,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2367,6 +2451,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2489,6 +2574,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2575,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3040,6 +3128,92 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CEKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata cekdata;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+
+				cekdata = (Form_pg_colenckeydata) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("column encryption key data of %s for %s"),
+								 getObjectDescription(&(ObjectAddress){ColumnEncKeyRelationId, cekdata->ckdcekid}, false),
+								 getObjectDescription(&(ObjectAddress){ColumnMasterKeyRelationId, cekdata->ckdcmkid}, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4440,6 +4614,18 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "collation");
 			break;
 
+		case OCLASS_CEK:
+			appendStringInfoString(&buffer, "column encryption key");
+			break;
+
+		case OCLASS_CEKDATA:
+			appendStringInfoString(&buffer, "column encryption key data");
+			break;
+
+		case OCLASS_CMK:
+			appendStringInfoString(&buffer, "column master key");
+			break;
+
 		case OCLASS_CONSTRAINT:
 			getConstraintTypeDescription(&buffer, object->objectId,
 										 missing_ok);
@@ -4905,6 +5091,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata form;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckeydata) GETSTRUCT(tup);
+				appendStringInfo(&buffer,
+								 "of %s for %s",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column master key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91..69f6175c60 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -19,6 +19,7 @@ OBJS = \
 	analyze.o \
 	async.o \
 	cluster.o \
+	colenccmds.o \
 	collationcmds.o \
 	comment.o \
 	constraint.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index bea51b3af1..59c23e9ef8 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -22,7 +22,9 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_foreign_data_wrapper.h"
@@ -118,6 +120,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -379,6 +387,8 @@ ExecRenameStmt(RenameStmt *stmt)
 			return RenameType(stmt);
 
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_EVENT_TRIGGER:
@@ -527,6 +537,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -643,6 +655,9 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 			break;
 
 		case OCLASS_CAST:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
@@ -876,6 +891,8 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/colenccmds.c b/src/backend/commands/colenccmds.c
new file mode 100644
index 0000000000..3ada6d5aeb
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,439 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/colenccmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+static void
+parse_cek_attributes(ParseState *pstate, List *definition, Oid *cmkoid_p, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	Assert(cmkoid_p);
+
+	foreach(lc, definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "column_master_key") == 0)
+			defelp = &cmkEl;
+		else if (strcmp(defel->defname, "algorithm") == 0)
+			defelp = &algEl;
+		else if (strcmp(defel->defname, "encrypted_value") == 0)
+			defelp = &encvalEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column encryption key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (cmkEl)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("attribute \"%s\" must be specified",
+						"column_master_key")));
+
+	if (algEl)
+	{
+		char	   *val = defGetString(algEl);
+
+		if (!alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"algorithm")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	if (encvalEl)
+	{
+		if (!encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"encrypted_value")));
+
+		encval = defGetString(encvalEl);
+	}
+	else
+	{
+		if (encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"encrypted_value")));
+	}
+
+	*cmkoid_p = cmkoid;
+	if (alg_p)
+		*alg_p = alg;
+	if (encval_p)
+		*encval_p = encval;
+}
+
+static void
+insert_cekdata_record(Oid cekoid, Oid cmkoid, int alg, char *encval)
+{
+	Oid			cekdataoid;
+	Relation	rel;
+	Datum		values[Natts_pg_colenckeydata] = {0};
+	bool		nulls[Natts_pg_colenckeydata] = {0};
+	HeapTuple	tup;
+	ObjectAddress myself;
+	ObjectAddress other;
+
+	rel = table_open(ColumnEncKeyDataRelationId, RowExclusiveLock);
+
+	cekdataoid = GetNewOidWithIndex(rel, ColumnEncKeyDataOidIndexId, Anum_pg_colenckeydata_oid);
+	values[Anum_pg_colenckeydata_oid - 1] = ObjectIdGetDatum(cekdataoid);
+	values[Anum_pg_colenckeydata_ckdcekid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckeydata_ckdcmkid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colenckeydata_ckdcmkalg - 1] = Int32GetDatum(alg);
+	values[Anum_pg_colenckeydata_ckdencval - 1] = DirectFunctionCall1(byteain, CStringGetDatum(encval));
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyDataRelationId, cekdataoid);
+
+	/* dependency cekdata -> cek */
+	ObjectAddressSet(other, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_AUTO);
+
+	/* dependency cekdata -> cmk */
+	ObjectAddressSet(other, ColumnMasterKeyRelationId, cmkoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_NORMAL);
+
+	table_close(rel, NoLock);
+}
+
+ObjectAddress
+CreateCEK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+	nulls[Anum_pg_colenckey_cekacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOnOwner(ColumnEncKeyRelationId, cekoid, GetUserId());
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnEncKeyRelationId, cekoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt)
+{
+	Oid			cekoid;
+	ObjectAddress address;
+
+	cekoid = get_cek_oid(stmt->cekname, false);
+
+	if (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(stmt->cekname));
+
+	if (stmt->isDrop)
+	{
+		Oid			cmkoid = 0;
+		Oid			cekdataoid;
+		ObjectAddress obj;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, NULL, NULL);
+		cekdataoid = get_cekdata_oid(cekoid, cmkoid, false);
+		ObjectAddressSet(obj, ColumnEncKeyDataRelationId, cekdataoid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+		AclResult	aclresult;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		if (get_cekdata_oid(cekoid, cmkoid, true))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("column encryption key \"%s\" already has data for master key \"%s\"",
+						   NameListToString(stmt->cekname), get_cmk_name(cmkoid, false)));
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	InvokeObjectPostAlterHook(ColumnEncKeyRelationId, cekoid, 0);
+	ObjectAddressSet(address, ColumnEncKeyRelationId, cekoid);
+
+	return address;
+}
+
+ObjectAddress
+CreateCMK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+		realm = defGetString(realmEl);
+	else
+		realm = "";
+
+	cmkoid = GetNewOidWithIndex(rel, ColumnMasterKeyOidIndexId, Anum_pg_colmasterkey_oid);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+	nulls[Anum_pg_colmasterkey_cmkacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	recordDependencyOnOwner(ColumnMasterKeyRelationId, cmkoid, GetUserId());
+
+	ObjectAddressSet(myself, ColumnMasterKeyRelationId, cmkoid);
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d6c6d514f3..9685b7886a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 296dc82d2e..86d22ca065 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/printtup.h"
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "commands/async.h"
@@ -25,7 +26,7 @@
 static void DiscardAll(bool isTopLevel);
 
 /*
- * DISCARD { ALL | SEQUENCES | TEMP | PLANS }
+ * DISCARD
  */
 void
 DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
@@ -36,6 +37,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			DiscardAll(isTopLevel);
 			break;
 
+		case DISCARD_COLUMN_ENCRYPTION_KEYS:
+			DiscardColumnEncryptionKeys();
+			break;
+
 		case DISCARD_PLANS:
 			ResetPlanCache();
 			break;
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	DiscardColumnEncryptionKeys();
 }
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 82bda15889..b4c681005a 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +517,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..4c1628cf7b 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,9 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -1027,6 +1030,9 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_TYPE:
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_CONVERSION:
 		case OCLASS_DEFAULT:
@@ -2056,6 +2062,9 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2139,6 +2148,9 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 42cced9ebe..4b5ac30441 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..93d61c96ad 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,9 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3e2c5f797c..11fa8e741c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,16 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			AclResult	aclresult;
+
+			GetColumnEncryption(colDef->encryption, attr);
+			aclresult = object_aclcheck(ColumnEncKeyRelationId, attr->attcek, GetUserId(), ACL_USAGE);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attr->attcek, false));
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -2569,6 +2582,33 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								attributeName)));
 				def = (ColumnDef *) list_nth(inhSchema, exist_attno - 1);
 
+				/*
+				 * Check encryption parameter.  All parents must have the same
+				 * encryption settings for a column.
+				 */
+				if ((def->encryption && !attribute->attcek) ||
+					(!def->encryption && attribute->attcek))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && attribute->attcek)
+				{
+					/*
+					 * Merging the encryption properties of two encrypted
+					 * parent columns is not yet implemented.  Right now, this
+					 * would confuse the checks of the type etc. below (we
+					 * must check the physical and the real types against each
+					 * other, respectively), which might require a larger
+					 * restructuring.  For now, just give up here.
+					 */
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("multiple inheritance of encrypted columns is not implemented")));
+				}
+
 				/*
 				 * Must have the same type and typmod
 				 */
@@ -2661,6 +2701,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+														attribute->attusertypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -2950,6 +2996,34 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								 errdetail("%s versus %s", def->compression, newdef->compression)));
 				}
 
+				/*
+				 * Check encryption parameter.  All parents and children must
+				 * have the same encryption settings for a column.
+				 */
+				if ((def->encryption && !newdef->encryption) ||
+					(!def->encryption && newdef->encryption))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && newdef->encryption)
+				{
+					FormData_pg_attribute a, newa;
+
+					GetColumnEncryption(def->encryption, &a);
+					GetColumnEncryption(newdef->encryption, &newa);
+
+					if (a.atttypid != newa.atttypid ||
+						a.atttypmod != newa.atttypmod ||
+						a.attcek != newa.attcek)
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" has an encryption specification conflict",
+										attributeName)));
+				}
+
 				/*
 				 * Merge of NOT NULL constraints = OR 'em together
 				 */
@@ -6897,6 +6971,19 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attislocal = colDef->is_local;
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
+	if (colDef->encryption)
+	{
+		GetColumnEncryption(colDef->encryption, &attribute);
+		aclresult = object_aclcheck(ColumnEncKeyRelationId, attribute.attcek, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attribute.attcek, false));
+	}
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attusertypid = 0;
+		attribute.attusertypmod = -1;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12728,6 +12815,9 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
 			case OCLASS_COLLATION:
+			case OCLASS_CEK:
+			case OCLASS_CEKDATA:
+			case OCLASS_CMK:
 			case OCLASS_CONVERSION:
 			case OCLASS_LANGUAGE:
 			case OCLASS_LARGEOBJECT:
@@ -19328,3 +19418,110 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	List	   *cek = NULL;
+	Oid			cekoid;
+	bool		encdet = false;
+	int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+	foreach(lc, coldefencryption)
+	{
+		DefElem    *el = lfirst_node(DefElem, lc);
+
+		if (strcmp(el->defname, "column_encryption_key") == 0)
+			cek = defGetQualifiedName(el);
+		else if (strcmp(el->defname, "encryption_type") == 0)
+		{
+			char	   *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+			if (strcmp(val, "deterministic") == 0)
+				encdet = true;
+			else if (strcmp(val, "randomized") == 0)
+				encdet = false;
+			else
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption type: %s", val));
+		}
+		else if (strcmp(el->defname, "algorithm") == 0)
+		{
+			char	   *val = strVal(el->arg);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption algorithm: %s", val));
+		}
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized column encryption parameter: %s", el->defname));
+	}
+
+	if (!cek)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+				errmsg("column encryption key must be specified"));
+
+	cekoid = get_cek_oid(cek, false);
+
+	attr->attcek = cekoid;
+	attr->attusertypid = attr->atttypid;
+	attr->attusertypmod = attr->atttypmod;
+
+	/* override physical type */
+	if (encdet)
+		attr->atttypid = PG_ENCRYPTED_DETOID;
+	else
+		attr->atttypid = PG_ENCRYPTED_RNDOID;
+	get_typlenbyvalalign(attr->atttypid,
+						 &attr->attlen, &attr->attbyval, &attr->attalign);
+	attr->attstorage = get_typstorage(attr->atttypid);
+	attr->attcollation = InvalidOid;
+
+	attr->atttypmod = alg;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	List	   *result;
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	char	   *nspname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attr->attcek));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attr->attcek);
+
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+	nspname = get_namespace_name(form->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(form->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->atttypmod))),
+									-1));
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index f0f2e07655..c65f6a4388 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -25,6 +25,7 @@
 #include "access/xlogprefetcher.h"
 #include "catalog/pg_authid.h"
 #include "common/string.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "postmaster/postmaster.h"
@@ -706,7 +707,11 @@ check_client_encoding(char **newval, void **extra, GucSource source)
 	 */
 	if (PrepareClientEncoding(encoding) < 0)
 	{
-		if (IsTransactionState())
+		if (MyProcPort->column_encryption_enabled)
+			GUC_check_errdetail("Conversion between %s and %s is not possible when column encryption is enabled.",
+								canonical_name,
+								GetDatabaseEncodingName());
+		else if (IsTransactionState())
 		{
 			/* Must be a genuine no-such-conversion problem */
 			GUC_check_errcode(ERRCODE_FEATURE_NOT_SUPPORTED);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index ff98c773f5..171dca3803 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index dc8415a693..c81bc15128 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3983,6 +3983,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(coldef->compression))
 					return true;
+				if (WALK(coldef->encryption))
+					return true;
 				if (WALK(coldef->raw_default))
 					return true;
 				if (WALK(coldef->collClause))
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a0138382a1..a6039878fa 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,6 +420,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <list>	parse_toplevel stmtmulti routine_body_stmt_list
 				OptTableElementList TableElementList OptInherit definition
+				list_of_definitions
 				OptTypedTableElementList TypedTableElementList
 				reloptions opt_reloptions
 				OptWith opt_definition func_args func_args_list
@@ -592,6 +594,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableConstraint TableLikeClause
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression column_storage opt_column_storage
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_match
@@ -690,8 +693,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
 	DOUBLE_P DROP
 
-	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
-	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EACH ELSE ENABLE_P ENCODING ENCRYPTION ENCRYPTED END_P ENUM_P ESCAPE
+	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -708,13 +711,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	JOIN
 
-	KEY
+	KEY KEYS
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
-	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+	MAPPING MASTER MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
 
 	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -1988,7 +1993,7 @@ CheckPointStmt:
 
 /*****************************************************************************
  *
- * DISCARD { ALL | TEMP | PLANS | SEQUENCES }
+ * DISCARD
  *
  *****************************************************************************/
 
@@ -2014,6 +2019,13 @@ DiscardStmt:
 					n->target = DISCARD_TEMP;
 					$$ = (Node *) n;
 				}
+			| DISCARD COLUMN ENCRYPTION KEYS
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+
+					n->target = DISCARD_COLUMN_ENCRYPTION_KEYS;
+					$$ = (Node *) n;
+				}
 			| DISCARD PLANS
 				{
 					DiscardStmt *n = makeNode(DiscardStmt);
@@ -3700,14 +3712,15 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_storage opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_encryption opt_column_storage opt_column_compression create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 
 					n->colname = $1;
 					n->typeName = $2;
-					n->storage_name = $3;
-					n->compression = $4;
+					n->encryption = $3;
+					n->storage_name = $4;
+					n->compression = $5;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3716,8 +3729,8 @@ columnDef:	ColId Typename opt_column_storage opt_column_compression create_gener
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $5;
-					SplitColQualList($6, &n->constraints, &n->collClause,
+					n->fdwoptions = $6;
+					SplitColQualList($7, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *) n;
@@ -3774,6 +3787,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4052,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6289,33 @@ DefineStmt:
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+			| CREATE COLUMN ENCRYPTION KEY any_name WITH VALUES list_of_definitions
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CEK;
+					n->defnames = $5;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name WITH definition
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = $7;
+					$$ = (Node *) n;
+				}
 		;
 
 definition: '(' def_list ')'						{ $$ = $2; }
@@ -6289,6 +6335,10 @@ def_elem:	ColLabel '=' def_arg
 				}
 		;
 
+list_of_definitions: definition						{ $$ = list_make1($1); }
+			| list_of_definitions ',' definition	{ $$ = lappend($1, $3); }
+		;
+
 /* Note: any simple identifier will be returned as a type name! */
 def_arg:	func_type						{ $$ = (Node *) $1; }
 			| reserved_keyword				{ $$ = (Node *) makeString(pstrdup($1)); }
@@ -6800,6 +6850,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -7611,6 +7663,24 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| COLUMN ENCRYPTION KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CEK;
+					n->objs = $4;
+					$$ = n;
+				}
+			| COLUMN MASTER KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CMK;
+					n->objs = $4;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9140,6 +9210,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9817,6 +9907,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10148,6 +10258,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11304,6 +11432,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_name ADD_P VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = false;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN ENCRYPTION KEY any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16965,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16840,6 +17015,7 @@ unreserved_keyword:
 			| INVOKER
 			| ISOLATION
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -16854,6 +17030,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17514,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17404,6 +17582,7 @@ bare_label_keyword:
 			| ISOLATION
 			| JOIN
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -17425,6 +17604,7 @@ bare_label_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index 2240284f21..a8a1855aa0 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,159 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			Expr	   *expr = te->expr;
+
+			/*
+			 * If it's a RelabelType, look inside.  (For encrypted columns,
+			 * this would typically be a typmod adjustment.)
+			 */
+			if (IsA(expr, RelabelType))
+				expr = castNode(RelabelType, expr)->arg;
+
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(expr, Param))
+			{
+				Param	   *p = (Param *) expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(expr, Var))
+			{
+				Var		   *v = (Var *) expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell   *lc;
+
+					foreach(lc, srcrte->values_lists)
+					{
+						List	   *values_list = lfirst_node(List, lc);
+						Expr	   *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, RelabelType))
+							value = castNode(RelabelType, value)->arg;
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach(lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f9218f48aa..8d902292cd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1035,8 +1035,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+												attribute->attusertypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2552327d90..79ec9c4d1f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2210,12 +2210,27 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 									valptr),
 							 errhint("Valid values are: \"false\", 0, \"true\", 1, \"database\".")));
 			}
+			else if (strcmp(nameptr, "_pq_.column_encryption") == 0)
+			{
+				/*
+				 * Right now, the only accepted value is "1".  This gives room
+				 * to expand this into a version number, for example.
+				 */
+				if (strcmp(valptr, "1") == 0)
+					port->column_encryption_enabled = true;
+				else
+					ereport(FATAL,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for parameter \"%s\": \"%s\"",
+									"column_encryption",
+									valptr),
+							 errhint("Valid values are: 1.")));
+			}
 			else if (strncmp(nameptr, "_pq_.", 5) == 0)
 			{
 				/*
 				 * Any option beginning with _pq_. is reserved for use as a
-				 * protocol-level option, but at present no such options are
-				 * defined.
+				 * protocol-level option.
 				 */
 				unrecognized_protocol_options =
 					lappend(unrecognized_protocol_options, pstrdup(nameptr));
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index cab709b07b..b577557384 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -71,6 +72,7 @@
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
 
@@ -1815,6 +1817,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (type_is_encrypted(ptype))
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter $%d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2560,6 +2572,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2618,11 +2632,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	for (int i = 0; i < psrc->num_params; i++)
 	{
 		Oid			ptype = psrc->param_types[i];
+		Oid			pcekid = InvalidOid;
+		int			pcekalg = 0;
+		int16		pflags = 0;
+
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(ptype))
+		{
+			Oid			porigtbl = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("parameter $%d corresponds to an encrypted column, but an underlying table and column could not be determined", i + 1)));
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(porigtbl), Int16GetDatum(porigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", porigcol, porigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			ptype = orig_att->attusertypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x0001;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&row_description_buf, (int) pcekid);
+			pq_sendint32(&row_description_buf, pcekalg);
+			pq_sendint16(&row_description_buf, pflags);
+		}
 	}
 	pq_endmessage_reuse(&row_description_buf);
 
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index c7d9d96b45..180f86cb79 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -30,6 +30,7 @@
 #include "commands/alter.h"
 #include "commands/async.h"
 #include "commands/cluster.h"
+#include "commands/colenccmds.h"
 #include "commands/collationcmds.h"
 #include "commands/comment.h"
 #include "commands/conversioncmds.h"
@@ -137,6 +138,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,14 @@ ProcessUtilitySlow(ParseState *pstate,
 															stmt->definition,
 															&secondaryObject);
 							break;
+						case OBJECT_CEK:
+							Assert(stmt->args == NIL);
+							address = CreateCEK(pstate, stmt);
+							break;
+						case OBJECT_CMK:
+							Assert(stmt->args == NIL);
+							address = CreateCMK(pstate, stmt);
+							break;
 						case OBJECT_COLLATION:
 							Assert(stmt->args == NIL);
 							address = DefineCollation(pstate,
@@ -1903,6 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,12 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_COLUMN:
 			tag = CMDTAG_ALTER_TABLE;
 			break;
+		case OBJECT_CEK:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+		case OBJECT_CMK:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
 		case OBJECT_CONVERSION:
 			tag = CMDTAG_ALTER_CONVERSION;
 			break;
@@ -2640,6 +2665,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_DROP_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_DROP_COLUMN_MASTER_KEY;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2760,6 +2791,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_COLLATION:
 					tag = CMDTAG_CREATE_COLLATION;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_CREATE_COLUMN_MASTER_KEY;
+					break;
 				case OBJECT_ACCESS_METHOD:
 					tag = CMDTAG_CREATE_ACCESS_METHOD;
 					break;
@@ -2917,6 +2954,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_ALL:
 					tag = CMDTAG_DISCARD_ALL;
 					break;
+				case DISCARD_COLUMN_ENCRYPTION_KEYS:
+					tag = CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS;
+					break;
 				case DISCARD_PLANS:
 					tag = CMDTAG_DISCARD_PLANS;
 					break;
@@ -3063,6 +3103,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3736,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 8f7522d103..6aeb06f8fd 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -22,6 +22,8 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_foreign_data_wrapper.h"
 #include "catalog/pg_foreign_server.h"
@@ -101,6 +103,10 @@ static AclMode convert_table_priv_string(text *priv_type_text);
 static AclMode convert_sequence_priv_string(text *priv_type_text);
 static AttrNumber convert_column_name(Oid tableoid, text *column);
 static AclMode convert_column_priv_string(text *priv_type_text);
+static Oid	convert_column_encryption_key_name(text *cekname);
+static AclMode convert_column_encryption_key_priv_string(text *priv_type_text);
+static Oid	convert_column_master_key_name(text *cmkname);
+static AclMode convert_column_master_key_priv_string(text *priv_type_text);
 static Oid	convert_database_name(text *databasename);
 static AclMode convert_database_priv_string(text *priv_type_text);
 static Oid	convert_foreign_data_wrapper_name(text *fdwname);
@@ -800,6 +806,14 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			/* for backwards compatibility, grant some rights by default */
 			world_default = ACL_CREATE_TEMP | ACL_CONNECT;
@@ -911,6 +925,12 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 's':
 			objtype = OBJECT_SEQUENCE;
 			break;
+		case 'Y':
+			objtype = OBJECT_CEK;
+			break;
+		case 'y':
+			objtype = OBJECT_CMK;
+			break;
 		case 'd':
 			objtype = OBJECT_DATABASE;
 			break;
@@ -2915,6 +2935,384 @@ convert_column_priv_string(text *priv_type_text)
 }
 
 
+/*
+ * has_column_encryption_key_privilege variants
+ *		These are all named "has_column_encryption_key_privilege" at the SQL level.
+ *		They take various combinations of column encryption key name,
+ *		cek OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_encryption_key_privilege_name_name
+ *		Check user privileges on a column encryption key given
+ *		name username, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name
+ *		Check user privileges on a column encryption key given
+ *		text cekname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cekname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name_id
+ *		Check user privileges on a column encryption key given
+ *		name usename, column encryption key oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id
+ *		Check user privileges on a column encryption key given
+ *		column encryption key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cekid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_name
+ *		Check user privileges on a column encryption key given
+ *		roleid, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_id
+ *		Check user privileges on a column encryption key given
+ *		roleid, cek oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_encryption_key_privilege family.
+ */
+
+/*
+ * Given a CEK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_encryption_key_name(text *cekname)
+{
+	return get_cek_oid(textToQualifiedNameList(cekname), false);
+}
+
+/*
+ * convert_column_encryption_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_encryption_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_encryption_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_encryption_key_priv_map);
+}
+
+
+/*
+ * has_column_master_key_privilege variants
+ *		These are all named "has_column_master_key_privilege" at the SQL level.
+ *		They take various combinations of column master key name,
+ *		cmk OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_master_key_privilege_name_name
+ *		Check user privileges on a column master key given
+ *		name username, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name
+ *		Check user privileges on a column master key given
+ *		text cmkname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cmkname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name_id
+ *		Check user privileges on a column master key given
+ *		name usename, column master key oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id
+ *		Check user privileges on a column master key given
+ *		column master key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cmkid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_name
+ *		Check user privileges on a column master key given
+ *		roleid, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_id
+ *		Check user privileges on a column master key given
+ *		roleid, cmk oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_master_key_privilege family.
+ */
+
+/*
+ * Given a CMK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_master_key_name(text *cmkname)
+{
+	return get_cmk_oid(textToQualifiedNameList(cmkname), false);
+}
+
+/*
+ * convert_column_master_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_master_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_master_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_master_key_priv_map);
+}
+
+
 /*
  * has_database_privilege variants
  *		These are all named "has_database_privilege" at the SQL level.
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 5778e3f0ef..dd21fcf59e 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -679,6 +679,113 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	Node	   *escontext = fcinfo->context;
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c07382051d..7ad159110f 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -24,7 +24,10 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_namespace.h"
@@ -2658,6 +2661,25 @@ type_is_multirange(Oid typid)
 	return (get_typtype(typid) == TYPTYPE_MULTIRANGE);
 }
 
+bool
+type_is_encrypted(Oid typid)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+		bool		result;
+
+		result = (typtup->typcategory == TYPCATEGORY_ENCRYPTED);
+		ReleaseSysCache(tp);
+		return result;
+	}
+	else
+		return false;
+}
+
 /*
  * get_type_category_preferred
  *
@@ -3683,3 +3705,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+char *
+get_cek_name(Oid cekid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cekname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+		return NULL;
+	}
+
+	cekname = pstrdup(NameStr(((Form_pg_colenckey) GETSTRUCT(tup))->cekname));
+
+	ReleaseSysCache(tup);
+
+	return cekname;
+}
+
+Oid
+get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok)
+{
+	Oid			cekdataid;
+
+	cekdataid = GetSysCacheOid2(CEKDATACEKCMK, Anum_pg_colenckeydata_oid,
+								ObjectIdGetDatum(cekid),
+								ObjectIdGetDatum(cmkid));
+	if (!OidIsValid(cekdataid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" has no data for master key \"%s\"",
+						get_cek_name(cekid, false), get_cmk_name(cmkid, false))));
+
+	return cekdataid;
+}
+
+char *
+get_cmk_name(Oid cmkid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cmkname;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+		return NULL;
+	}
+
+	cmkname = pstrdup(NameStr(((Form_pg_colmasterkey) GETSTRUCT(tup))->cmkname));
+
+	ReleaseSysCache(tup);
+
+	return cmkname;
+}
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 77c2ba3f8f..d40af13efe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 94abede512..eb432260da 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -219,6 +222,32 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAMENSP] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameNspIndexId,
+		KEY(Anum_pg_colenckey_cekname,
+			Anum_pg_colenckey_ceknamespace),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +262,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAMENSP] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameNspIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname,
+			Anum_pg_colmasterkey_cmknamespace),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 033647011b..4b6be68f27 100644
--- a/src/backend/utils/mb/mbutils.c
+++ b/src/backend/utils/mb/mbutils.c
@@ -36,7 +36,9 @@
 
 #include "access/xact.h"
 #include "catalog/namespace.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
@@ -130,6 +132,12 @@ PrepareClientEncoding(int encoding)
 		encoding == PG_SQL_ASCII)
 		return 0;
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	if (IsTransactionState())
 	{
 		/*
@@ -237,6 +245,12 @@ SetClientEncoding(int encoding)
 		return 0;
 	}
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	/*
 	 * Search the cache for the entry previously prepared by
 	 * PrepareClientEncoding; if there isn't one, we lose.  While at it,
@@ -297,7 +311,9 @@ InitializeClientEncoding(void)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("conversion between %s and %s is not supported",
 						pg_enc2name_tbl[pending_client_encoding].name,
-						GetDatabaseEncodingName())));
+						GetDatabaseEncodingName()),
+				 (MyProcPort->column_encryption_enabled) ?
+				 errdetail("Encoding conversion is not possible when column encryption is enabled.") : 0));
 	}
 
 	/*
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index a2b74901e4..f35df16080 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -201,6 +203,12 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading user-defined collations");
 	(void) getCollations(fout, &numCollations);
 
+	pg_log_info("reading column master keys");
+	getColumnMasterKeys(fout);
+
+	pg_log_info("reading column encryption keys");
+	getColumnEncryptionKeys(fout);
+
 	pg_log_info("reading user-defined conversions");
 	getConversions(fout, &numConversions);
 
@@ -876,6 +884,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 079693585c..eeed0db211 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -484,6 +484,10 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('U', "USAGE");
 	}
+	else if (strcmp(type, "COLUMN ENCRYPTION KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "COLUMN MASTER KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "DATABASE") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,7 @@ typedef struct _connParams
 	char	   *pghost;
 	char	   *username;
 	trivalue	promptPassword;
+	int			column_encryption;
 	/* If not NULL, this overrides the dbname obtained from command line */
 	/* (but *only* the DB name, not anything else in the connstring) */
 	char	   *override_dbname;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 61ebb8fe85..bc303550fd 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3396,6 +3396,8 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 
 	/* objects that don't require special decoration */
 	if (strcmp(type, "COLLATION") == 0 ||
+		strcmp(type, "COLUMN ENCRYPTION KEY") == 0 ||
+		strcmp(type, "COLUMN MASTER KEY") == 0 ||
 		strcmp(type, "CONVERSION") == 0 ||
 		strcmp(type, "DOMAIN") == 0 ||
 		strcmp(type, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 100644
--- a/src/bin/pg_dump/pg_backup_db.c
+++ b/src/bin/pg_dump/pg_backup_db.c
@@ -133,8 +133,8 @@ ConnectDatabase(Archive *AHX,
 	 */
 	do
 	{
-		const char *keywords[8];
-		const char *values[8];
+		const char *keywords[9];
+		const char *values[9];
 		int			i = 0;
 
 		/*
@@ -159,6 +159,11 @@ ConnectDatabase(Archive *AHX,
 		}
 		keywords[i] = "fallback_application_name";
 		values[i++] = progname;
+		if (cparams->column_encryption)
+		{
+			keywords[i] = "column_encryption";
+			values[i++] = "1";
+		}
 		keywords[i] = NULL;
 		values[i++] = NULL;
 		Assert(i <= lengthof(keywords));
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4217908f84..f57bccee23 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,8 @@ static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
 static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
 static void dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo);
 static void dumpCollation(Archive *fout, const CollInfo *collinfo);
+static void dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo);
+static void dumpColumnMasterKey(Archive *fout, const CmkInfo *cekinfo);
 static void dumpConversion(Archive *fout, const ConvInfo *convinfo);
 static void dumpRule(Archive *fout, const RuleInfo *rinfo);
 static void dumpAgg(Archive *fout, const AggInfo *agginfo);
@@ -393,6 +396,7 @@ main(int argc, char **argv)
 		{"attribute-inserts", no_argument, &dopt.column_inserts, 1},
 		{"binary-upgrade", no_argument, &dopt.binary_upgrade, 1},
 		{"column-inserts", no_argument, &dopt.column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &dopt.cparams.column_encryption, 1},
 		{"disable-dollar-quoting", no_argument, &dopt.disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &dopt.disable_triggers, 1},
 		{"enable-row-security", no_argument, &dopt.enable_row_security, 1},
@@ -685,6 +689,9 @@ main(int argc, char **argv)
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
 	 */
+	if (dopt.cparams.column_encryption && dopt.dump_inserts == 0)
+		pg_fatal("option --decrypt-encrypted-columns requires option --inserts, --rows-per-insert, or --column-inserts");
+
 	if (dopt.do_nothing && dopt.dump_inserts == 0)
 		pg_fatal("option --on-conflict-do-nothing requires option --inserts, --rows-per-insert, or --column-inserts");
 
@@ -1056,6 +1063,7 @@ help(const char *progname)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security (dump only content user has\n"
@@ -5587,6 +5595,164 @@ getCollations(Archive *fout, int *numCollations)
 	return collinfo;
 }
 
+/*
+ * getColumnEncryptionKeys
+ *	  get information about column encryption keys
+ */
+void
+getColumnEncryptionKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CekInfo	   *cekinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cekname;
+	int			i_ceknamespace;
+	int			i_cekowner;
+	int			i_cekacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, cek.cekowner, cek.cekacl, acldefault('Y', cek.cekowner) AS acldefault\n"
+					  "FROM pg_colenckey cek");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cekname = PQfnumber(res, "cekname");
+	i_ceknamespace = PQfnumber(res, "ceknamespace");
+	i_cekowner = PQfnumber(res, "cekowner");
+	i_cekacl = PQfnumber(res, "cekacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cekinfo = pg_malloc(ntups * sizeof(CekInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		PGresult	   *res2;
+		int				ntups2;
+
+		cekinfo[i].dobj.objType = DO_CEK;
+		cekinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cekinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cekinfo[i].dobj);
+		cekinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cekname));
+		cekinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cekacl));
+		cekinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cekinfo[i].dacl.privtype = 0;
+		cekinfo[i].dacl.initprivs = NULL;
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, ckdcmkalg, ckdencval\n"
+						  "FROM pg_catalog.pg_colenckeydata\n"
+						  "WHERE ckdcekid = %u", cekinfo[i].dobj.catId.oid);
+		res2 = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		ntups2 = PQntuples(res2);
+		cekinfo[i].numdata = ntups2;
+		cekinfo[i].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			cekinfo[i].cekcmkalgs[j] = atoi(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkalg")));
+			cekinfo[i].cekencvals[j] = pg_strdup(PQgetvalue(res2, j, PQfnumber(res2, "ckdencval")));
+		}
+		PQclear(res2);
+
+		selectDumpableObject(&(cekinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cekacl))
+			cekinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * getColumnMasterKeys
+ *	  get information about column master keys
+ */
+void
+getColumnMasterKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CmkInfo	   *cmkinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cmkname;
+	int			i_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+	int			i_cmkacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, cmk.cmkowner, cmk.cmkrealm, cmk.cmkacl, acldefault('y', cmk.cmkowner) AS acldefault\n"
+					  "FROM pg_colmasterkey cmk");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cmkname = PQfnumber(res, "cmkname");
+	i_cmknamespace = PQfnumber(res, "cmknamespace");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+	i_cmkacl = PQfnumber(res, "cmkacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cmkinfo = pg_malloc(ntups * sizeof(CmkInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		cmkinfo[i].dobj.objType = DO_CMK;
+		cmkinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cmkinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cmkinfo[i].dobj);
+		cmkinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cmkname));
+		cmkinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		cmkinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cmkacl));
+		cmkinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cmkinfo[i].dacl.privtype = 0;
+		cmkinfo[i].dacl.initprivs = NULL;
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cmkacl))
+			cmkinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8202,6 +8368,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8261,8 +8430,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
+	appendPQExpBuffer(q, "SELECT\n"
 						 "a.attrelid,\n"
 						 "a.attnum,\n"
 						 "a.attname,\n"
@@ -8275,7 +8443,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attlen,\n"
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
+						 "pg_catalog.format_type(%s) AS atttypname,\n"
 						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
@@ -8284,7 +8452,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "' ' || pg_catalog.quote_literal(option_value) "
 						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
 						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+						 "), E',\n    ') AS attfdwoptions,\n",
+						 fout->remoteVersion >= 160000 ?
+						 "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END, CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END" :
+						 "a.atttypid, a.atttypmod");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -8310,10 +8481,23 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	if (fout->remoteVersion >= 120000)
 		appendPQExpBufferStr(q,
-							 "a.attgenerated\n");
+							 "a.attgenerated,\n");
+	else
+		appendPQExpBufferStr(q,
+							 "'' AS attgenerated,\n");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBuffer(q,
+						  "a.attcek,\n"
+						  "CASE a.atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "CASE WHEN a.atttypid IN (%u, %u) THEN a.atttypmod END AS attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID,
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcek,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
@@ -8338,6 +8522,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	i_attencdet = PQfnumber(res, "attencdet");
+	i_attencalg = PQfnumber(res, "attencalg");
 	i_attisdropped = PQfnumber(res, "attisdropped");
 	i_attlen = PQfnumber(res, "attlen");
 	i_attalign = PQfnumber(res, "attalign");
@@ -8398,6 +8585,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		tbinfo->typstorage = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attidentity = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attgenerated = (char *) pg_malloc(numatts * sizeof(char));
+		tbinfo->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		tbinfo->attencdet = (bool *) pg_malloc(numatts * sizeof(bool));
+		tbinfo->attencalg = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char));
@@ -8425,6 +8615,22 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->attidentity[j] = *(PQgetvalue(res, r, i_attidentity));
 			tbinfo->attgenerated[j] = *(PQgetvalue(res, r, i_attgenerated));
 			tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
+			if (!PQgetisnull(res, r, i_attcek))
+			{
+				Oid		attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[j] = NULL;
+			if (!PQgetisnull(res, r, i_attencdet))
+				tbinfo->attencdet[j] = (PQgetvalue(res, r, i_attencdet)[0] == 't');
+			else
+				tbinfo->attencdet[j] = 0;
+			if (!PQgetisnull(res, r, i_attencalg))
+				tbinfo->attencalg[j] = atoi(PQgetvalue(res, r, i_attencalg));
+			else
+				tbinfo->attencalg[j] = 0;
 			tbinfo->attisdropped[j] = (PQgetvalue(res, r, i_attisdropped)[0] == 't');
 			tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
 			tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
@@ -9943,6 +10149,12 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_OPFAMILY:
 			dumpOpfamily(fout, (const OpfamilyInfo *) dobj);
 			break;
+		case DO_CEK:
+			dumpColumnEncryptionKey(fout, (const CekInfo *) dobj);
+			break;
+		case DO_CMK:
+			dumpColumnMasterKey(fout, (const CmkInfo *) dobj);
+			break;
 		case DO_COLLATION:
 			dumpCollation(fout, (const CollInfo *) dobj);
 			break;
@@ -13357,6 +13569,141 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * dumpColumnEncryptionKey
+ *	  dump the definition of the given column encryption key
+ */
+static void
+dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcekname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcekname = pg_strdup(fmtId(cekinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN ENCRYPTION KEY %s;\n",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_name(cekinfo->cekcmkalgs[i]));
+		appendPQExpBuffer(query, "encrypted_value = ");
+		appendStringLiteralAH(query, cekinfo->cekencvals[i], fout);
+
+		appendPQExpBuffer(query, ")");
+		if (i < cekinfo->numdata - 1)
+			appendPQExpBuffer(query, ", ");
+	}
+
+	appendPQExpBufferStr(query, ";\n");
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cekinfo->dobj.catId, cekinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cekinfo->dobj.name,
+								  .namespace = cekinfo->dobj.namespace->dobj.name,
+								  .owner = cekinfo->rolname,
+								  .description = "COLUMN ENCRYPTION KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cekinfo->dobj.dumpId, InvalidDumpId, "COLUMN ENCRYPTION KEY",
+				qcekname, NULL, cekinfo->dobj.namespace->dobj.name,
+				cekinfo->rolname, &cekinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcekname);
+}
+
+/*
+ * dumpColumnMasterKey
+ *	  dump the definition of the given column master key
+ */
+static void
+dumpColumnMasterKey(Archive *fout, const CmkInfo *cmkinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcmkname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcmkname = pg_strdup(fmtId(cmkinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN MASTER KEY %s;\n",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "realm = ");
+	appendStringLiteralAH(query, cmkinfo->cmkrealm, fout);
+
+	appendPQExpBufferStr(query, ");\n");
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cmkinfo->dobj.catId, cmkinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cmkinfo->dobj.name,
+								  .namespace = cmkinfo->dobj.namespace->dobj.name,
+								  .owner = cmkinfo->rolname,
+								  .description = "COLUMN MASTER KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN MASTER KEY", qcmkname,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cmkinfo->dobj.dumpId, InvalidDumpId, "COLUMN MASTER KEY",
+				qcmkname, NULL, cmkinfo->dobj.namespace->dobj.name,
+				cmkinfo->rolname, &cmkinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15442,6 +15789,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[j]));
+						/*
+						 * To reduce output size, we don't print the default
+						 * of encryption_type, but we do print the default of
+						 * algorithm, since we might want to change to a new
+						 * default algorithm sometime in the future.
+						 */
+						if (tbinfo->attencdet[j])
+							appendPQExpBuffer(q, "encryption_type = deterministic, ");
+						appendPQExpBuffer(q, "algorithm = '%s')",
+										  get_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -18010,6 +18373,8 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_ACCESS_METHOD:
 			case DO_OPCLASS:
 			case DO_OPFAMILY:
+			case DO_CEK:
+			case DO_CMK:
 			case DO_COLLATION:
 			case DO_CONVERSION:
 			case DO_TABLE:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index cdca0b993d..d4a2e595d0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -47,6 +47,8 @@ typedef enum
 	DO_ACCESS_METHOD,
 	DO_OPCLASS,
 	DO_OPFAMILY,
+	DO_CEK,
+	DO_CMK,
 	DO_COLLATION,
 	DO_CONVERSION,
 	DO_TABLE,
@@ -332,6 +334,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	struct _CekInfo **attcek;
+	int		   *attencalg;
+	bool	   *attencdet;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
 	char	   *attalign;		/* attribute align, used by binary_upgrade */
 	bool	   *attislocal;		/* true if attr has local definition */
@@ -663,6 +668,32 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	struct _CmkInfo **cekcmks;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -683,6 +714,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -710,6 +743,8 @@ extern AccessMethodInfo *getAccessMethods(Archive *fout, int *numAccessMethods);
 extern OpclassInfo *getOpclasses(Archive *fout, int *numOpclasses);
 extern OpfamilyInfo *getOpfamilies(Archive *fout, int *numOpfamilies);
 extern CollInfo *getCollations(Archive *fout, int *numCollations);
+extern void getColumnEncryptionKeys(Archive *fout);
+extern void getColumnMasterKeys(Archive *fout);
 extern ConvInfo *getConversions(Archive *fout, int *numConversions);
 extern TableInfo *getTables(Archive *fout, int *numTables);
 extern void getOwnedSeqs(Archive *fout, TableInfo tblinfo[], int numTables);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 8266c117a3..d3dacd39da 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -69,6 +69,8 @@ enum dbObjectTypePriorities
 	PRIO_TSTEMPLATE,
 	PRIO_TSDICT,
 	PRIO_TSCONFIG,
+	PRIO_CMK,
+	PRIO_CEK,
 	PRIO_FDW,
 	PRIO_FOREIGN_SERVER,
 	PRIO_TABLE,
@@ -111,6 +113,8 @@ static const int dbObjectTypePriority[] =
 	PRIO_ACCESS_METHOD,			/* DO_ACCESS_METHOD */
 	PRIO_OPFAMILY,				/* DO_OPCLASS */
 	PRIO_OPFAMILY,				/* DO_OPFAMILY */
+	PRIO_CEK,					/* DO_CEK */
+	PRIO_CMK,					/* DO_CMK */
 	PRIO_COLLATION,				/* DO_COLLATION */
 	PRIO_CONVERSION,			/* DO_CONVERSION */
 	PRIO_TABLE,					/* DO_TABLE */
@@ -1322,6 +1326,16 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "OPERATOR FAMILY %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_CEK:
+			snprintf(buf, bufsize,
+					 "COLUMN ENCRYPTION KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
+		case DO_CMK:
+			snprintf(buf, bufsize,
+					 "COLUMN MASTER KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_COLLATION:
 			snprintf(buf, bufsize,
 					 "COLLATION %s  (ID %d OID %u)",
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index cd421c5944..6530fb81a2 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -93,6 +93,7 @@ static bool dosync = true;
 
 static int	binary_upgrade = 0;
 static int	column_inserts = 0;
+static int	decrypt_encrypted_columns = 0;
 static int	disable_dollar_quoting = 0;
 static int	disable_triggers = 0;
 static int	if_exists = 0;
@@ -154,6 +155,7 @@ main(int argc, char *argv[])
 		{"attribute-inserts", no_argument, &column_inserts, 1},
 		{"binary-upgrade", no_argument, &binary_upgrade, 1},
 		{"column-inserts", no_argument, &column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &decrypt_encrypted_columns, 1},
 		{"disable-dollar-quoting", no_argument, &disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &disable_triggers, 1},
 		{"exclude-database", required_argument, NULL, 6},
@@ -424,6 +426,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --binary-upgrade");
 	if (column_inserts)
 		appendPQExpBufferStr(pgdumpopts, " --column-inserts");
+	if (decrypt_encrypted_columns)
+		appendPQExpBufferStr(pgdumpopts, " --decrypt-encrypted-columns");
 	if (disable_dollar_quoting)
 		appendPQExpBufferStr(pgdumpopts, " --disable-dollar-quoting");
 	if (disable_triggers)
@@ -649,6 +653,7 @@ help(void)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --exclude-database=PATTERN   exclude databases whose name matches PATTERN\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 187e4b8d07..9a8c032c97 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -719,6 +719,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
 	'ALTER FOREIGN DATA WRAPPER dummy OWNER TO' => {
 		regexp => qr/^ALTER FOREIGN DATA WRAPPER dummy OWNER TO .+;/m,
 		like   => { %full_runs, section_pre_data => 1, },
@@ -1319,6 +1331,26 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1737,6 +1769,26 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3570,6 +3622,26 @@
 		unlike => { no_privs => 1, },
 	},
 
+	'GRANT USAGE ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
+	'GRANT USAGE ON COLUMN MASTER KEY cmk1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
 	'GRANT USAGE ON FOREIGN DATA WRAPPER dummy' => {
 		create_order => 85,
 		create_sql   => 'GRANT USAGE ON FOREIGN DATA WRAPPER dummy
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..0d6a46d24b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 99e28f607e..8f21fabd99 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1539,7 +1539,7 @@ describeOneTableDetails(const char *schemaname,
 	bool		printTableInitialized = false;
 	int			i;
 	char	   *view_def = NULL;
-	char	   *headers[12];
+	char	   *headers[13];
 	PQExpBufferData title;
 	PQExpBufferData tmpbuf;
 	int			cols;
@@ -1555,6 +1555,7 @@ describeOneTableDetails(const char *schemaname,
 				fdwopts_col = -1,
 				attstorage_col = -1,
 				attcompression_col = -1,
+				attcekname_col = -1,
 				attstattarget_col = -1,
 				attdescr_col = -1;
 	int			numrows;
@@ -1577,6 +1578,8 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attusertypid;
+	const char *attusertypmod;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1853,7 +1856,17 @@ describeOneTableDetails(const char *schemaname,
 	cols = 0;
 	printfPQExpBuffer(&buf, "SELECT a.attname");
 	attname_col = cols++;
-	appendPQExpBufferStr(&buf, ",\n  pg_catalog.format_type(a.atttypid, a.atttypmod)");
+	if (pset.sversion >= 160000)
+	{
+		attusertypid = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END";
+		attusertypmod = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END";
+	}
+	else
+	{
+		attusertypid = "a.atttypid";
+		attusertypmod = "a.atttypmod";
+	}
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, %s)", attusertypid, attusertypmod);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1866,7 +1879,8 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		attrdef_col = cols++;
 		attnotnull_col = cols++;
-		appendPQExpBufferStr(&buf, ",\n  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
+		appendPQExpBufferStr(&buf, ",\n"
+							 "  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
 							 "   WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) AS attcollation");
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
@@ -1918,6 +1932,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			appendPQExpBufferStr(&buf, ",\n  (SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek) AS attcekname");
+			attcekname_col = cols++;
+		}
+
 		/* stats target, if relevant to relkind */
 		if (tableinfo.relkind == RELKIND_RELATION ||
 			tableinfo.relkind == RELKIND_INDEX ||
@@ -2041,6 +2067,8 @@ describeOneTableDetails(const char *schemaname,
 		headers[cols++] = gettext_noop("Storage");
 	if (attcompression_col >= 0)
 		headers[cols++] = gettext_noop("Compression");
+	if (attcekname_col >= 0)
+		headers[cols++] = gettext_noop("Encryption");
 	if (attstattarget_col >= 0)
 		headers[cols++] = gettext_noop("Stats target");
 	if (attdescr_col >= 0)
@@ -2133,6 +2161,17 @@ describeOneTableDetails(const char *schemaname,
 							  false, false);
 		}
 
+		/* Column encryption */
+		if (attcekname_col >= 0)
+		{
+			if (!PQgetisnull(res, i, attcekname_col))
+				printTableAddCell(&cont, PQgetvalue(res, i, attcekname_col),
+								  false, false);
+			else
+				printTableAddCell(&cont, "",
+								  false, false);
+		}
+
 		/* Statistics target, if the relkind supports this feature */
 		if (attstattarget_col >= 0)
 			printTableAddCell(&cont, PQgetvalue(res, i, attstattarget_col),
@@ -4486,6 +4525,152 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cekacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cmkacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cmk.oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 554fe86725..1cf8f72176 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1729966959 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,8 @@ helpVariables(unsigned short int pager)
 		  "    true if last query failed, else false\n");
 	HELP0("  FETCH_COUNT\n"
 		  "    the number of result rows to fetch and display at a time (0 = unlimited)\n");
+	HELP0("  HIDE_COLUMN_ENCRYPTION\n"
+		  "    if set, column encryption details are not displayed\n");
 	HELP0("  HIDE_TABLEAM\n"
 		  "    if set, table access methods are not displayed\n");
 	HELP0("  HIDE_TOAST_COMPRESSION\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 73d4b393bc..010bc5a6d5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,7 @@ typedef struct _psqlSettings
 	bool		quiet;
 	bool		singleline;
 	bool		singlestep;
+	bool		hide_column_encryption;
 	bool		hide_compression;
 	bool		hide_tableam;
 	int			fetch_count;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 5a28b6f713..6736505c3a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -1188,6 +1188,13 @@ hide_compression_hook(const char *newval)
 							 &pset.hide_compression);
 }
 
+static bool
+hide_column_encryption_hook(const char *newval)
+{
+	return ParseVariableBool(newval, "HIDE_COLUMN_ENCRYPTION",
+							 &pset.hide_column_encryption);
+}
+
 static bool
 hide_tableam_hook(const char *newval)
 {
@@ -1259,6 +1266,9 @@ EstablishVariableSpace(void)
 	SetVariableHooks(pset.vars, "SHOW_CONTEXT",
 					 show_context_substitute_hook,
 					 show_context_hook);
+	SetVariableHooks(pset.vars, "HIDE_COLUMN_ENCRYPTION",
+					 bool_substitute_hook,
+					 hide_column_encryption_hook);
 	SetVariableHooks(pset.vars, "HIDE_TOAST_COMPRESSION",
 					 bool_substitute_hook,
 					 hide_compression_hook);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 8f12af799b..2f0ca71981 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -933,6 +933,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1223,6 +1237,8 @@ static const pgsql_thing_t words_after_create[] = {
 	{"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", NULL, NULL, &Query_for_list_of_collations},
+	{"COLUMN ENCRYPTION KEY", NULL, NULL, NULL},
+	{"COLUMN MASTER KEY KEY", NULL, NULL, NULL},
 
 	/*
 	 * CREATE CONSTRAINT TRIGGER is not supported here because it is designed
@@ -1705,7 +1721,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1952,6 +1968,22 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "COLLATION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "REFRESH VERSION", "RENAME TO", "SET SCHEMA");
 
+	/* ALTER/DROP COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "ENCRYPTION", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_ceks);
+
+	/* ALTER COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("ADD VALUE (", "DROP VALUE (", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2894,6 +2926,26 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("true", "false");
 	}
 
+	/* CREATE/ALTER/DROP COLUMN ... KEY */
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN"))
+		COMPLETE_WITH("ENCRYPTION", "MASTER");
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN", "ENCRYPTION|MASTER"))
+		COMPLETE_WITH("KEY");
+
+	/* CREATE COLUMN ENCRYPTION KEY */
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("VALUES");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH", "VALUES"))
+		COMPLETE_WITH("(");
+
+	/* CREATE COLUMN MASTER KEY */
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("(");
+
 	/* CREATE DATABASE */
 	else if (Matches("CREATE", "DATABASE", MatchAny))
 		COMPLETE_WITH("OWNER", "TEMPLATE", "ENCODING", "TABLESPACE",
@@ -3605,7 +3657,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 	else if (Matches("DISCARD"))
-		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP");
+		COMPLETE_WITH("ALL", "COLUMN ENCRYPTION KEYS", "PLANS", "SEQUENCES", "TEMP");
 
 /* DO */
 	else if (Matches("DO"))
@@ -3619,6 +3671,7 @@ psql_completion(const char *text, int start, int end)
 			 Matches("DROP", "ACCESS", "METHOD", MatchAny) ||
 			 (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
 			  ends_with(prev_wd, ')')) ||
+			 Matches("DROP", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
 			 Matches("DROP", "EVENT", "TRIGGER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "TABLE", MatchAny) ||
@@ -3931,6 +3984,8 @@ psql_completion(const char *text, int start, int end)
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"COLUMN ENCRYPTION KEY",
+											"COLUMN MASTER KEY",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4046,6 +4101,16 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+	/* Complete "GRANT/REVOKE * ON COLUMN ENCRYPTION|MASTER KEY *" with TO/FROM */
+	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
+			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny))
+	{
+		if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny, MatchAny))
+			COMPLETE_WITH("TO");
+		else
+			COMPLETE_WITH("FROM");
+	}
+
 	/* Complete "GRANT/REVOKE * ON FOREIGN DATA WRAPPER *" with TO/FROM */
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny))
diff --git a/src/common/Makefile b/src/common/Makefile
index 113029bf7b..73dce1150e 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			return "AEAD_AES_128_CBC_HMAC_SHA_256";
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			return "AEAD_AES_192_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			return "AEAD_AES_256_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			return "AEAD_AES_256_CBC_HMAC_SHA_512";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index 41bd58ebdf..3695d3285b 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -4,6 +4,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 747ecb800d..4e384bbcdb 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,9 +20,13 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
+extern void DiscardColumnEncryptionKeys(void);
+
 extern void debugStartup(DestReceiver *self, int operation,
 						 TupleDesc typeinfo);
 extern bool debugtup(TupleTableSlot *slot, DestReceiver *self);
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index ffd5e9dc82..eb59e73c0a 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -92,6 +92,9 @@ typedef enum ObjectClass
 	OCLASS_TYPE,				/* pg_type */
 	OCLASS_CAST,				/* pg_cast */
 	OCLASS_COLLATION,			/* pg_collation */
+	OCLASS_CEK,					/* pg_colenckey */
+	OCLASS_CEKDATA,				/* pg_colenckeydata */
+	OCLASS_CMK,					/* pg_colmasterkey */
 	OCLASS_CONSTRAINT,			/* pg_constraint */
 	OCLASS_CONVERSION,			/* pg_conversion */
 	OCLASS_DEFAULT,				/* pg_attrdef */
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index d01ab504b6..758696b539 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 3179be09d3..9e2c5256f7 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,9 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_colmasterkey.h',
+  'pg_colenckey.h',
+  'pg_colenckeydata.h',
 ]
 
 bki_data = [
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index f64a0ec26b..d0b9e8458d 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -115,6 +115,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c4d6adcd3e..c58b79e3a7 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5b950129de..0e9e85ebf3 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index b561e17781..7910175a6a 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,17 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	bool		attislocal BKI_DEFAULT(t);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey);
+
+	/*
+	 * User-visible type and typmod, currently used for encrypted columns.
+	 * These are only set to nondefault values if they are different from
+	 * atttypid and attypmod.
+	 */
+	Oid			attusertypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+	int32		attusertypmod BKI_DEFAULT(-1);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..c57fa18a27
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+/* ----------------
+ *		pg_colenckey definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckey
+ * ----------------
+ */
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+	Oid			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		cekacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_TOAST(pg_colenckey, 8263, 8264);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_nsp_index, 8242, ColumnEncKeyNameNspIndexId, on pg_colenckey using btree(cekname name_ops, ceknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkeydata.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+/* ----------------
+ *		pg_colenckeydata definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckeydata
+ * ----------------
+ */
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..d3bfd36279
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+	aclitem		cmkacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, on pg_colmasterkey using btree(cmkname name_ops, cmknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c867d99563..ff06d52fd0 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index b2cdea66c4..114279fa64 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 91587b99d0..c21052a3f7 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 505595620e..b410388a42 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6355,6 +6355,14 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -7142,6 +7150,68 @@
   proname => 'fmgr_sql_validator', provolatile => 's', prorettype => 'void',
   proargtypes => 'oid', prosrc => 'fmgr_sql_validator' },
 
+{ oid => '8265',
+  descr => 'user privilege on column encryption key by username, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_encryption_key_privilege_name_name' },
+{ oid => '8266',
+  descr => 'user privilege on column encryption key by username, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_encryption_key_privilege_name_id' },
+{ oid => '8267',
+  descr => 'user privilege on column encryption key by user oid, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_encryption_key_privilege_id_name' },
+{ oid => '8268',
+  descr => 'user privilege on column encryption key by user oid, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_encryption_key_privilege_id_id' },
+{ oid => '8269',
+  descr => 'current user privilege on column encryption key by column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_encryption_key_privilege_name' },
+{ oid => '8270',
+  descr => 'current user privilege on column encryption key by column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_encryption_key_privilege_id' },
+
+{ oid => '8271',
+  descr => 'user privilege on column master key by username, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_master_key_privilege_name_name' },
+{ oid => '8272',
+  descr => 'user privilege on column master key by username, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_master_key_privilege_name_id' },
+{ oid => '8273',
+  descr => 'user privilege on column master key by user oid, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_master_key_privilege_id_name' },
+{ oid => '8274',
+  descr => 'user privilege on column master key by user oid, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_master_key_privilege_id_id' },
+{ oid => '8275',
+  descr => 'current user privilege on column master key by column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_master_key_privilege_name' },
+{ oid => '8276',
+  descr => 'current user privilege on column master key by column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_master_key_privilege_id' },
+
 { oid => '2250',
   descr => 'user privilege on database by username, database name',
   proname => 'has_database_privilege', provolatile => 's', prorettype => 'bool',
@@ -11939,4 +12009,37 @@
   proname => 'any_value_transfn', prorettype => 'anyelement',
   proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8245',
+  proname => 'pg_encrypted_det_eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'pg_encrypted_det_ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 92bcaf2c73..b9d3920a97 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,17 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+# Note: typstorage 'e' since compression is not useful for encrypted data
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 519e570c8c..3c7ab2a8fe 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -294,6 +294,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPCATEGORY_USER		'U'
 #define  TYPCATEGORY_BITSTRING	'V' /* er ... "varbit"? */
 #define  TYPCATEGORY_UNKNOWN	'X'
+#define  TYPCATEGORY_ENCRYPTED	'Y'
 #define  TYPCATEGORY_INTERNAL	'Z'
 
 #define  TYPALIGN_CHAR			'c' /* char alignment (i.e. unaligned) */
diff --git a/src/include/commands/colenccmds.h b/src/include/commands/colenccmds.h
new file mode 100644
index 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/colenccmds.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COLENCCMDS_H
+#define COLENCCMDS_H
+
+#include "catalog/objectaddress.h"
+#include "parser/parse_node.h"
+
+extern ObjectAddress CreateCEK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt);
+extern ObjectAddress CreateCMK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e7c2b91a58..11f88c59ce 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,4 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml;.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index ac6407e9f6..39a286e58a 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -164,6 +164,7 @@ typedef struct Port
 	 */
 	char	   *database_name;
 	char	   *user_name;
+	bool		column_encryption_enabled;
 	char	   *cmdline_options;
 	List	   *guc_options;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 371aa0ffc5..4253d531f8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -721,6 +721,7 @@ typedef struct ColumnDef
 	char	   *colname;		/* name of column */
 	TypeName   *typeName;		/* type of column */
 	char	   *compression;	/* compression method for column */
+	List	   *encryption;		/* encryption info for column */
 	int			inhcount;		/* number of times column is inherited */
 	bool		is_local;		/* column has local (non-inherited) def'n */
 	bool		is_not_null;	/* NOT NULL constraint specified? */
@@ -757,11 +758,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1979,6 +1981,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2166,6 +2171,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
@@ -3644,6 +3674,7 @@ typedef struct CheckPointStmt
 typedef enum DiscardMode
 {
 	DISCARD_ALL,
+	DISCARD_COLUMN_ENCRYPTION_KEYS,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
 	DISCARD_TEMP
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..c6da932042 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -149,6 +149,7 @@ PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("encryption", ENCRYPTION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -228,6 +229,7 @@ PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL)
 PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("language", LANGUAGE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("large", LARGE_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -250,6 +252,7 @@ PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("master", MASTER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index d4865e50f6..26e1cd617a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..a5e61c9f5b 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -29,6 +29,8 @@ PG_CMDTAG(CMDTAG_ALTER_ACCESS_METHOD, "ALTER ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_AGGREGATE, "ALTER AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CAST, "ALTER CAST", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_COLLATION, "ALTER COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY, "ALTER COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_MASTER_KEY, "ALTER COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONSTRAINT, "ALTER CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONVERSION, "ALTER CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_DATABASE, "ALTER DATABASE", false, false, false)
@@ -86,6 +88,8 @@ PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, fals
 PG_CMDTAG(CMDTAG_CREATE_AGGREGATE, "CREATE AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CAST, "CREATE CAST", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_COLLATION, "CREATE COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY, "CREATE COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_MASTER_KEY, "CREATE COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONSTRAINT, "CREATE CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONVERSION, "CREATE CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_DATABASE, "CREATE DATABASE", false, false, false)
@@ -130,6 +134,7 @@ PG_CMDTAG(CMDTAG_DECLARE_CURSOR, "DECLARE CURSOR", false, false, false)
 PG_CMDTAG(CMDTAG_DELETE, "DELETE", false, false, true)
 PG_CMDTAG(CMDTAG_DISCARD, "DISCARD", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
+PG_CMDTAG(CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS, "DISCARD COLUMN ENCRYPTION KEYS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false)
@@ -138,6 +143,8 @@ PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CAST, "DROP CAST", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_COLLATION, "DROP COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_ENCRYPTION_KEY, "DROP COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_MASTER_KEY, "DROP COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONSTRAINT, "DROP CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONVERSION, "DROP CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_DATABASE, "DROP DATABASE", false, false, false)
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2..0c73022833 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -159,6 +159,8 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
+#define ACL_ALL_RIGHTS_CEK			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_CMK			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..1dac24e4b4 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -163,6 +163,7 @@ extern bool type_is_rowtype(Oid typid);
 extern bool type_is_enum(Oid typid);
 extern bool type_is_range(Oid typid);
 extern bool type_is_multirange(Oid typid);
+extern bool type_is_encrypted(Oid typid);
 extern void get_type_category_preferred(Oid typid,
 										char *typcategory,
 										bool *typispreferred);
@@ -202,6 +203,9 @@ extern Oid	get_publication_oid(const char *pubname, bool missing_ok);
 extern char *get_publication_name(Oid pubid, bool missing_ok);
 extern Oid	get_subscription_oid(const char *subname, bool missing_ok);
 extern char *get_subscription_name(Oid subid, bool missing_ok);
+extern char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok);
+extern char *get_cmk_name(Oid cmkid, bool missing_ok);
 
 #define type_is_array(typid)  (get_element_type(typid) != InvalidOid)
 /* type_is_array_domain accepts both plain arrays and domains over arrays */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index a443181d41..8ecacb29df 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d5d50ceab4..bf1d527c1a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAMENSP,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAMENSP,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c18e914228..10dfda8016 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,7 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 97e47f0585..24e326b448 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -341,6 +341,14 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	{"cmklookup", "PGCMKLOOKUP", "", NULL,
+		"CMK-Lookup", "", 64,
+	offsetof(struct pg_conn, cmklookup)},
+
+	{"column_encryption", "PGCOLUMNENCRYPTION", "0", NULL,
+		"Column-Encryption", "", 1,
+	offsetof(struct pg_conn, column_encryption_setting)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -1412,6 +1420,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4056,6 +4086,22 @@ freePGconn(PGconn *conn)
 	free(conn->krbsrvname);
 	free(conn->gsslib);
 	free(conn->connip);
+	free(conn->cmklookup);
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		free(conn->cmks[i].cmkname);
+		free(conn->cmks[i].cmkrealm);
+	}
+	free(conn->cmks);
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		if (conn->ceks[i].cekdata)
+		{
+			explicit_bzero(conn->ceks[i].cekdata, conn->ceks[i].cekdatalen);
+			free(conn->ceks[i].cekdata);
+		}
+	}
+	free(conn->ceks);
 	/* Note that conn->Pfdebug is not ours to close or free */
 	free(conn->write_err_msg);
 	free(conn->inBuffer);
diff --git a/src/interfaces/libpq/fe-encrypt-openssl.c b/src/interfaces/libpq/fe-encrypt-openssl.c
new file mode 100644
index 0000000000..9baa47da13
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,839 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt-openssl.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe-encrypt.h"
+#include "libpq-int.h"
+
+#include "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+#define pqResultAlloc(res, nBytes, isBinary) malloc(nBytes)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = NULL;
+	EVP_PKEY_CTX *ctx = NULL;
+	unsigned char *out = NULL;
+	size_t		outlen;
+
+	switch (cmkalg)
+	{
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			md = EVP_sha1();
+			break;
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		free(out);
+		out = NULL;
+		goto fail;
+	}
+
+	*tolen = outlen;
+
+fail:
+	EVP_PKEY_CTX_free(ctx);
+	EVP_PKEY_free(key);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05;
+ * for encrypting and decrypting column values.
+ */
+
+#ifdef TEST_ENCRYPT
+
+/*
+ * Test data from
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5;
+ */
+
+/*
+ * The different test cases just use different prefixes of K, so one constant
+ * is enough here.
+ */
+static const unsigned char K[] = {
+	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+	0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+	0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+	0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+};
+
+static const unsigned char P[] = {
+	0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20,
+	0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75,
+	0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65,
+	0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62,
+	0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69,
+	0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66,
+	0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f,
+	0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65,
+};
+
+static const unsigned char test_IV[] = {
+	0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04,
+};
+
+static const unsigned char test_A[] = {
+	0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63,
+	0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20,
+	0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+static const EVP_CIPHER *
+pg_cekalg_to_openssl_cipher(int cekalg)
+{
+	const EVP_CIPHER *cipher;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			cipher = EVP_aes_128_cbc();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			cipher = EVP_aes_192_cbc();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			cipher = EVP_aes_256_cbc();
+			break;
+		default:
+			cipher = NULL;
+	}
+
+	return cipher;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+static const EVP_MD *
+pg_cekalg_to_openssl_md(int cekalg)
+{
+	const EVP_MD *md;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			md = EVP_sha384();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			md = EVP_sha512();
+			break;
+		default:
+			md = NULL;
+	}
+
+	return md;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+static int
+md_key_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 16;
+	else if (md == EVP_sha384())
+		return 24;
+	else if (md == EVP_sha512())
+		return 32;
+	else
+		return -1;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+static int
+md_hash_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 32;
+	else if (md == EVP_sha384())
+		return 48;
+	else if (md == EVP_sha512())
+		return 64;
+	else
+		return -1;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+static bool
+get_message_auth_tag(const EVP_MD *md,
+					 const unsigned char *mac_key, int mac_key_len,
+					 const unsigned char *encr, int encrlen,
+					 unsigned char *md_value, size_t *md_len_p,
+					 const char **errmsgp)
+{
+	static char msgbuf[1024];
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	int64		al;
+	bool		result = false;
+
+	if (encrlen < 0)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("encrypted value has invalid length"));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, mac_key, mac_key_len);
+	if (!pkey)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("could not allocate key for HMAC: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, buf, bufsize))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, md_len_p))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	Assert(*md_len_p == md_hash_length(md));
+
+	/* truncate output to half the length, per spec */
+	*md_len_p /= 2;
+
+	result = true;
+fail:
+	free(buf);
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+
+/*
+ * Decrypt a column value
+ */
+unsigned char *
+decrypt_value(PGresult *res, const PGCEK *cek, int cekalg, const unsigned char *input, int inputlen, const char **errmsgp)
+{
+	static char msgbuf[1024];
+
+	const unsigned char *iv = NULL;
+	size_t		ivlen;
+
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *decr;
+	int			decrlen,
+				decrlen2;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized encryption algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized digest algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("column encryption key has wrong length for algorithm (has: %zu, required: %d)"),
+				 cek->cekdatalen, key_len);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_memcmp(input + (inputlen - md_len), md_value, md_len) != 0)
+	{
+		*errmsgp = libpq_gettext("MAC mismatch");
+		goto fail;
+	}
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	iv = input;
+	input += ivlen;
+	inputlen -= ivlen;
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = inputlen + EVP_CIPHER_CTX_block_size(evp_cipher_ctx) + 1;
+	buf = pqResultAlloc(res, bufsize, false);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	decr = buf;
+	if (!EVP_DecryptUpdate(evp_cipher_ctx, decr, &decrlen, input, inputlen - md_len))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	if (!EVP_DecryptFinal_ex(evp_cipher_ctx, decr + decrlen, &decrlen2))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	decrlen += decrlen2;
+	Assert(decrlen < bufsize);
+	decr[decrlen] = '\0';
+	result = decr;
+
+fail:
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	return result;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#ifndef TEST_ENCRYPT
+static bool
+make_siv(PGconn *conn,
+		 unsigned char *iv, size_t ivlen,
+		 const EVP_MD *md,
+		 const unsigned char *iv_key, int iv_key_len,
+		 const unsigned char *plaintext, int plaintext_len)
+{
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	bool		result = false;
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, iv_key, iv_key_len);
+	if (!pkey)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	Assert(md_len == md_hash_length(md));
+	memcpy(iv, md_value, ivlen);
+
+	result = true;
+fail:
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+#endif							/* TEST_ENCRYPT */
+
+/*
+ * Encrypt a column value
+ */
+unsigned char *
+encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg, const unsigned char *value, int *nbytesp, bool enc_det)
+{
+	int			nbytes = *nbytesp;
+	unsigned char iv[EVP_MAX_IV_LENGTH];
+	size_t		ivlen;
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_key;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *encr;
+	int			encrlen,
+				encrlen2;
+
+	const char *errmsg;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		buf2size;
+	unsigned char *buf2 = NULL;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	Assert(ivlen <= sizeof(iv));
+	if (enc_det)
+	{
+#ifndef TEST_ENCRYPT
+		make_siv(conn, iv, ivlen, md, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	bufsize = ivlen + (nbytes + 2 * EVP_CIPHER_CTX_block_size(evp_cipher_ctx) - 1);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	encrlen += encrlen2;
+
+	encr -= ivlen;
+	encrlen += ivlen;
+
+	Assert(encrlen <= bufsize);
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  encr, encrlen,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf2, encr, encrlen);
+	memcpy(buf2 + encrlen, md_value, md_len);
+
+	result = buf2;
+	nbytes = buf2size;
+
+fail:
+	free(buf);
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	*nbytesp = nbytes;
+	return result;
+}
+
+
+/*
+ * Run test cases
+ */
+#ifdef TEST_ENCRYPT
+
+static void
+debug_print_hex(const char *name, const unsigned char *val, int len)
+{
+	printf("%s =", name);
+	for (int i = 0; i < len; i++)
+	{
+		if (i % 16 == 0)
+			printf("\n");
+		else
+			printf(" ");
+		printf("%02x", val[i]);
+	}
+	printf("\n");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, K_len);
+
+	C = encrypt_value(NULL, &cek, alg, P, &nbytes, true);
+	debug_print_hex("C", C, nbytes);
+}
+
+int
+main(int argc, char **argv)
+{
+	printf("5.1\n");
+	test_case(PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256, K, 32, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FE_ENCRYPT_H
+#define FE_ENCRYPT_H
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+extern unsigned char *decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+											int fromlen, const unsigned char *from,
+											int *tolen);
+
+extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg,
+									const unsigned char *input, int inputlen,
+									const char **errmsgp);
+
+extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg,
+									const unsigned char *value, int *nbytesp, bool enc_det);
+
+#endif							/* FE_ENCRYPT_H */
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index ec62550e38..4bd9e4e309 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,420 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+int
+pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+					  const char *keyrealm)
+{
+	char	   *keyname_copy;
+	char	   *keyrealm_copy;
+	bool		found;
+
+	keyname_copy = strdup(keyname);
+	if (!keyname_copy)
+		return EOF;
+	keyrealm_copy = strdup(keyrealm);
+	if (!keyrealm_copy)
+	{
+		free(keyname_copy);
+		return EOF;
+	}
+
+	found = false;
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		struct pg_cmk *checkcmk = &conn->cmks[i];
+
+		/* replace existing? */
+		if (checkcmk->cmkid == keyid)
+		{
+			free(checkcmk->cmkname);
+			free(checkcmk->cmkrealm);
+			checkcmk->cmkname = keyname_copy;
+			checkcmk->cmkrealm = keyrealm_copy;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newncmks;
+		struct pg_cmk *newcmks;
+		struct pg_cmk *newcmk;
+
+		newncmks = conn->ncmks + 1;
+		if (newncmks <= 0)
+			return EOF;
+		newcmks = realloc(conn->cmks, newncmks * sizeof(struct pg_cmk));
+		if (!newcmks)
+		{
+			free(keyname_copy);
+			free(keyrealm_copy);
+			return EOF;
+		}
+
+		newcmk = &newcmks[newncmks - 1];
+		newcmk->cmkid = keyid;
+		newcmk->cmkname = keyname_copy;
+		newcmk->cmkrealm = keyrealm_copy;
+
+		conn->ncmks = newncmks;
+		conn->cmks = newcmks;
+	}
+
+	return 0;
+}
+
+/*
+ * Replace placeholders in input string.  Return value malloc'ed.
+ */
+static char *
+replace_cmk_placeholders(const char *in, const char *cmkname, const char *cmkrealm, int cmkalg, const char *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					p++;
+					break;
+				case 'r':
+					appendPQExpBufferStr(&buf, cmkrealm);
+					p++;
+					break;
+				default:
+					appendPQExpBufferChar(&buf, p[0]);
+			}
+		}
+		else
+			appendPQExpBufferChar(&buf, p[0]);
+	}
+
+	return buf.data;
+}
+
+#ifndef USE_SSL
+/*
+ * Dummy implementation for non-SSL builds
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	return NULL;
+}
+#endif
+
+/*
+ * Decrypt a CEK using the given CMK.  The ciphertext is passed in
+ * "from" and "fromlen".  Return the decrypted value in a malloc'ed area, its
+ * length via "tolen".  Return NULL on error; add error messages directly to
+ * "conn".
+ */
+static unsigned char *
+decrypt_cek(PGconn *conn, const PGCMK *cmk, int cmkalg,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	char	   *cmklookup;
+	bool		found = false;
+	unsigned char *result = NULL;
+
+	if (!conn->cmklookup || !conn->cmklookup[0])
+	{
+		libpq_append_conn_error(conn, "column master key lookup is not configured");
+		return NULL;
+	}
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		return NULL;
+	}
+
+	/*
+	 * Analyze semicolon-separated list
+	 */
+	for (char *s = strtok(cmklookup, ";"); s; s = strtok(NULL, ";"))
+	{
+		char	   *sep;
+
+		/* split found token at '=' */
+		sep = strchr(s, '=');
+		if (!sep)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', s);
+			break;
+		}
+
+		/* matching realm? */
+		if (strncmp(s, "*", sep - s) == 0 || strncmp(s, cmk->cmkrealm, sep - s) == 0)
+		{
+			char	   *sep2;
+
+			found = true;
+
+			sep2 = strchr(sep, ':');
+			if (!sep2)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, PG_BINARY_R);
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+int
+pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, const unsigned char *value, int len)
+{
+	PGCMK	   *cmk = NULL;
+	unsigned char *plainval = NULL;
+	int			plainvallen = 0;
+	bool		found;
+
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		if (conn->cmks[i].cmkid == cmkid)
+		{
+			cmk = &conn->cmks[i];
+			break;
+		}
+	}
+
+	if (!cmk)
+		return EOF;
+
+	plainval = decrypt_cek(conn, cmk, cmkalg, len, value, &plainvallen);
+	if (!plainval)
+		return EOF;
+
+	found = false;
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		struct pg_cek *checkcek = &conn->ceks[i];
+
+		/* replace existing? */
+		if (checkcek->cekid == keyid)
+		{
+			free(checkcek->cekdata);
+			checkcek->cekdata = plainval;
+			checkcek->cekdatalen = plainvallen;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newnceks;
+		struct pg_cek *newceks;
+		struct pg_cek *newcek;
+
+		newnceks = conn->nceks + 1;
+		if (newnceks <= 0)
+		{
+			free(plainval);
+			return EOF;
+		}
+		newceks = realloc(conn->ceks, newnceks * sizeof(struct pg_cek));
+		if (!newceks)
+		{
+			free(plainval);
+			return EOF;
+		}
+
+		newcek = &newceks[newnceks - 1];
+		newcek->cekid = keyid;
+		newcek->cekdata = plainval;
+		newcek->cekdatalen = plainvallen;
+
+		conn->nceks = newnceks;
+		conn->ceks = newceks;
+	}
+
+	return 0;
+}
 
 /*
  * pqRowProcessor
@@ -1251,13 +1668,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == res->attDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					*errmsgp = libpq_gettext("protocol error: column encryption key associated with encrypted column was not sent by the server");
+					goto fail;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				if (val == NULL)
+					goto fail;
+#else
+				*errmsgp = libpq_gettext("column encryption not supported by this build");
 				goto fail;
+#endif
+			}
+			else
+			{
+				val = (char *) pqResultAlloc(res, clen + 1, isbinary);
+				if (val == NULL)
+					goto fail;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* copy and zero-terminate the data (even if it's binary) */
+				memcpy(val, columns[i].value, clen);
+				val[clen] = '\0';
+			}
 
 			tup[i].len = clen;
 			tup[i].value = val;
@@ -1500,6 +1955,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1973,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2012,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2128,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2171,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2270,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2319,47 @@ PQsendQueryGuts(PGconn *conn,
 		goto sendFailed;
 
 	/* Send parameter formats */
-	if (nParams > 0 && paramFormats)
+	if (nParams > 0 && (paramFormats || (paramDesc && paramDesc->paramDescs)))
 	{
 		if (pqPutInt(nParams, 2, conn) < 0)
 			goto sendFailed;
+
 		for (i = 0; i < nParams; i++)
 		{
-			if (pqPutInt(paramFormats[i], 2, conn) < 0)
+			int			format = paramFormats ? paramFormats[i] : 0;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2378,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2396,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				bool		enc_det = (paramDesc->paramDescs[i].flags & 0x01) != 0;
+				PGCEK	   *cek = NULL;
+				char	   *enc_paramValue;
+				int			enc_nbytes = nbytes;
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == paramDesc->paramDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					libpq_append_conn_error(conn, "protocol error: column encryption key associated with encrypted parameter was not sent by the server");
+					goto sendFailed;
+				}
+
+				enc_paramValue = (char *) encrypt_value(conn, cek, paramDesc->paramDescs[i].cekalg,
+														(const unsigned char *) paramValue, &enc_nbytes, enc_det);
+				if (!enc_paramValue)
+					goto sendFailed;
+
+				if (pqPutInt(enc_nbytes, 4, conn) < 0 ||
+					pqPutnchar(enc_paramValue, enc_nbytes, conn) < 0)
+					goto sendFailed;
+
+				free(enc_paramValue);
+#else
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2878,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4146,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4194,17 @@ PQfmod(const PGresult *res, int field_num)
 		return 0;
 }
 
+int
+PQfisencrypted(const PGresult *res, int field_num)
+{
+	if (!check_field_number(res, field_num))
+		return false;
+	if (res->attDescs)
+		return (res->attDescs[field_num].cekid != 0);
+	else
+		return false;
+}
+
 char *
 PQcmdStatus(PGresult *res)
 {
@@ -3762,6 +4390,17 @@ PQparamtype(const PGresult *res, int param_num)
 		return InvalidOid;
 }
 
+int
+PQparamisencrypted(const PGresult *res, int param_num)
+{
+	if (!check_param_number(res, param_num))
+		return false;
+	if (res->paramDescs)
+		return (res->paramDescs[param_num].cekid != 0);
+	else
+		return false;
+}
+
 
 /* PQsetnonblocking:
  *	sets the PGconn's database connection non-blocking if the arg is true
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 8ab6a88416..617e641cf6 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -43,6 +43,8 @@ static int	getRowDescriptions(PGconn *conn, int msgLength);
 static int	getParamDescriptions(PGconn *conn, int msgLength);
 static int	getAnotherTuple(PGconn *conn, int msgLength);
 static int	getParameterStatus(PGconn *conn);
+static int	getColumnMasterKey(PGconn *conn);
+static int	getColumnEncryptionKey(PGconn *conn);
 static int	getNotify(PGconn *conn);
 static int	getCopyStart(PGconn *conn, ExecStatusType copytype);
 static int	getReadyForQuery(PGconn *conn);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -358,8 +366,21 @@ pqParseInput3(PGconn *conn)
 					}
 					break;
 				case 't':		/* Parameter Description */
-					if (getParamDescriptions(conn, msgLength))
-						return;
+					if (conn->error_result ||
+						(conn->result != NULL &&
+						 conn->result->resultStatus == PGRES_FATAL_ERROR))
+					{
+						/*
+						 * We've already choked for some reason.  Just discard
+						 * the data till we get to the end of the query.
+						 */
+						conn->inCursor += msgLength;
+					}
+					else
+					{
+						if (getParamDescriptions(conn, msgLength))
+							return;
+					}
 					break;
 				case 'D':		/* Data Row */
 					if (conn->result != NULL &&
@@ -547,6 +568,9 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		int			typlen;
 		int			atttypmod;
 		int			format;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGets(&conn->workBuffer, conn) ||
 			pqGetInt(&tableid, 4, conn) ||
@@ -561,6 +585,23 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, conn) ||
+				pqGetInt(&flags, 2, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -582,6 +623,8 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		result->attDescs[i].typid = typid;
 		result->attDescs[i].typlen = typlen;
 		result->attDescs[i].atttypmod = atttypmod;
+		result->attDescs[i].cekid = cekid;
+		result->attDescs[i].cekalg = cekalg;
 
 		if (format != 1)
 			result->binary = 0;
@@ -685,10 +728,31 @@ getParamDescriptions(PGconn *conn, int msgLength)
 	for (i = 0; i < nparams; i++)
 	{
 		int			typid;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGetInt(&typid, 4, conn))
 			goto not_enough_data;
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&cekalg, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&flags, 2, conn))
+				goto not_enough_data;
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
 		result->paramDescs[i].typid = typid;
+		result->paramDescs[i].cekid = cekid;
+		result->paramDescs[i].cekalg = cekalg;
+		result->paramDescs[i].flags = flags;
 	}
 
 	/* Success! */
@@ -1468,6 +1532,92 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+/*
+ * Attempt to read a ColumnMasterKey message.
+ * Entry: 'y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	int			keyid;
+	char	   *keyname;
+	char	   *keyrealm;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	if (!keyname)
+		return EOF;
+	/* Get the key realm */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyrealm = strdup(conn->workBuffer.data);
+	if (!keyrealm)
+		return EOF;
+	/* And save it */
+	ret = pqSaveColumnMasterKey(conn, keyid, keyname, keyrealm);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(keyname);
+	free(keyrealm);
+
+	return ret;
+}
+
+/*
+ * Attempt to read a ColumnEncryptionKey message.
+ * Entry: 'Y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	int			keyid;
+	int			cmkid;
+	int			cmkalg;
+	char	   *buf;
+	int			vallen;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK ID */
+	if (pqGetInt(&cmkid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK algorithm */
+	if (pqGetInt(&cmkalg, 4, conn) != 0)
+		return EOF;
+	/* Get the key data len */
+	if (pqGetInt(&vallen, 4, conn) != 0)
+		return EOF;
+	/* Get the key data */
+	buf = malloc(vallen);
+	if (!buf)
+		return EOF;
+	if (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2436,9 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	if (conn->column_encryption_enabled)
+		ADD_STARTUP_OPTION("_pq_.column_encryption", "1");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index abaab6a073..5c2793a5eb 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -450,7 +450,8 @@ pqTraceOutputS(FILE *f, const char *message, int *cursor)
 
 /* ParameterDescription */
 static void
-pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -458,12 +459,21 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 	nfields = pqTraceOutputInt16(f, message, cursor);
 
 	for (int i = 0; i < nfields; i++)
+	{
 		pqTraceOutputInt32(f, message, cursor, regress);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+			pqTraceOutputInt16(f, message, cursor);
+		}
+	}
 }
 
 /* RowDescription */
 static void
-pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -479,6 +489,12 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
 		pqTraceOutputInt16(f, message, cursor);
 		pqTraceOutputInt32(f, message, cursor, false);
 		pqTraceOutputInt16(f, message, cursor);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+			pqTraceOutputInt16(f, message, cursor);
+		}
 	}
 }
 
@@ -514,6 +530,30 @@ pqTraceOutputW(FILE *f, const char *message, int *cursor, int length)
 		pqTraceOutputInt16(f, message, cursor);
 }
 
+/* ColumnMasterKey */
+static void
+pqTraceOutputy(FILE *f, const char *message, int *cursor, bool regress)
+{
+	fprintf(f, "ColumnMasterKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+}
+
+/* ColumnEncryptionKey */
+static void
+pqTraceOutputY(FILE *f, const char *message, int *cursor, bool regress)
+{
+	int			len;
+
+	fprintf(f, "ColumnEncryptionKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, false);
+	len = pqTraceOutputInt32(f, message, cursor, false);
+	pqTraceOutputNchar(f, len, message, cursor);
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -647,10 +687,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 				fprintf(conn->Pfdebug, "Sync"); /* no message content */
 			break;
 		case 't':				/* Parameter Description */
-			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'T':				/* Row Description */
-			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'v':				/* Negotiate Protocol Version */
 			pqTraceOutputv(conn->Pfdebug, message, &logCursor);
@@ -665,6 +707,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			fprintf(conn->Pfdebug, "Terminate");
 			/* No message content */
 			break;
+		case 'y':
+			pqTraceOutputy(conn->Pfdebug, message, &logCursor, regress);
+			break;
+		case 'Y':
+			pqTraceOutputY(conn->Pfdebug, message, &logCursor, regress);
+			break;
 		case 'Z':				/* Ready For Query */
 			pqTraceOutputZ(conn->Pfdebug, message, &logCursor);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f3d9220496..cf339a3e53 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -267,6 +267,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -438,6 +440,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,7 @@ extern int	PQfformat(const PGresult *res, int field_num);
 extern Oid	PQftype(const PGresult *res, int field_num);
 extern int	PQfsize(const PGresult *res, int field_num);
 extern int	PQfmod(const PGresult *res, int field_num);
+extern int	PQfisencrypted(const PGresult *res, int field_num);
 extern char *PQcmdStatus(PGresult *res);
 extern char *PQoidStatus(const PGresult *res);	/* old and ugly */
 extern Oid	PQoidValue(const PGresult *res);	/* new and improved */
@@ -540,6 +559,7 @@ extern int	PQgetlength(const PGresult *res, int tup_num, int field_num);
 extern int	PQgetisnull(const PGresult *res, int tup_num, int field_num);
 extern int	PQnparams(const PGresult *res);
 extern Oid	PQparamtype(const PGresult *res, int param_num);
+extern int	PQparamisencrypted(const PGresult *res, int param_num);
 
 /* Describe prepared statements and portals */
 extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d7ec5ed429..05b5891ffe 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -112,6 +112,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -343,6 +346,26 @@ typedef struct pg_conn_host
 								 * found in password file. */
 } pg_conn_host;
 
+/*
+ * Column encryption support data
+ */
+
+/* column master key */
+typedef struct pg_cmk
+{
+	Oid			cmkid;
+	char	   *cmkname;
+	char	   *cmkrealm;
+} PGCMK;
+
+/* column encryption key */
+typedef struct pg_cek
+{
+	Oid			cekid;
+	unsigned char *cekdata;		/* (decrypted) */
+	size_t		cekdatalen;
+} PGCEK;
+
 /*
  * PGconn stores all the state data associated with a single connection
  * to a backend.
@@ -396,6 +419,8 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *cmklookup;		/* CMK lookup specification */
+	char	   *column_encryption_setting;	/* column_encryption connection parameter */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -477,6 +502,13 @@ struct pg_conn
 	PGVerbosity verbosity;		/* error/notice message verbosity */
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
+	bool		column_encryption_enabled;	/* parsed version of column_encryption_setting */
+
+	/* Column encryption support data */
+	int			ncmks;
+	PGCMK	   *cmks;
+	int			nceks;
+	PGCEK	   *ceks;
 
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
@@ -673,6 +705,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern int	pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+								  const char *keyrealm);
+extern int	pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg,
+									  const unsigned char *value, int len);
 extern int	pqRowProcessor(PGconn *conn, const char **errmsgp);
 extern void pqCommandQueueAdvance(PGconn *conn);
 extern int	PQsendQueryContinue(PGconn *conn, const char *query);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 573fd9b6ea..a6e6b2ada9 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,7 @@ endif
 
 if ssl.found()
   libpq_sources += files('fe-secure-common.c')
+  libpq_sources += files('fe-encrypt-openssl.c')
   libpq_sources += files('fe-secure-openssl.c')
 endif
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': get_option('ssl')},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index b6ed455183..9aef1c7a29 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -3,6 +3,7 @@ CATALOG_NAME     = libpq
 GETTEXT_FILES    = fe-auth.c \
                    fe-auth-scram.c \
                    fe-connect.c \
+                   fe-encrypt-openssl.c \
                    fe-exec.c \
                    fe-gssapi-common.c \
                    fe-lobj.c \
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 100644
--- a/src/interfaces/libpq/test/Makefile
+++ b/src/interfaces/libpq/test/Makefile
@@ -15,10 +15,17 @@ override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
 LDFLAGS_INTERNAL += $(libpq_pgport)
 
 PROGS = libpq_testclient libpq_uri_regress
+ifeq ($(with_ssl),openssl)
+PROGS += libpq_test_encrypt
+endif
+
 
 all: $(PROGS)
 
 $(PROGS): $(WIN32RES)
 
+libpq_test_encrypt: ../fe-encrypt-openssl.c
+	$(CC) $(CPPFLAGS) -DTEST_ENCRYPT $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 clean distclean maintainer-clean:
 	rm -f $(PROGS) *.o
diff --git a/src/interfaces/libpq/test/meson.build b/src/interfaces/libpq/test/meson.build
index b2a4b06fd2..87d2808b52 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c9a3868053 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -24,7 +24,7 @@ ifeq ($(with_ldap),yes)
 SUBDIRS += ldap
 endif
 ifeq ($(with_ssl),openssl)
-SUBDIRS += ssl
+SUBDIRS += column_encryption ssl
 endif
 
 # Test suites that are not safe by default but can be run if selected
@@ -36,7 +36,7 @@ export PG_TEST_EXTRA
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),column_encryption examples kerberos icu ldap ssl)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..764cadf550
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/column_encryption/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+export OPENSSL
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/meson.build b/src/test/column_encryption/meson.build
new file mode 100644
index 0000000000..84cfa84e12
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,23 @@
+column_encryption_test_client = executable('test_client',
+  files('test_client.c'),
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+testprep_targets += column_encryption_test_client
+
+tests += {
+  'name': 'column_encryption',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_column_encryption.pl',
+      't/002_cmk_rotation.pl',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..8fa253524d
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,257 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+my $perlbin = $^X;
+$perlbin =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$openssl, 'pkeyutl', '-encrypt',
+		'-inkey', $cmkfilename,
+		'-pkeyopt', 'rsa_padding_mode:oaep',
+		'-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+		'-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+	# create CEK in database
+	$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = ${cmkname}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c smallint ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# Expected ciphertext length is 2 blocks of AES output (2 * 16) plus
+# half SHA-256 output (16) in hex encoding: (2 * 16 + 16) * 2 = 96
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1) TO STDOUT}),
+	qr/1\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text
+c|smallint),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result');
+
+{
+	local $ENV{PGCMKLOOKUP} = '*=run:broken %k %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = qq{*=run:"$perlbin" ./test_run_decrypt.pl %k %a %p};
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$node->command_fails_like(['test_client', 'test1'], qr/not encrypted/, 'test client fails because parameters not encrypted');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result after test client insert');
+
+$node->command_ok(['test_client', 'test2'], 'test client test 2');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3|33),
+	'decrypted query result after test client insert 2');
+
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1 WHERE a = 3) TO STDOUT}),
+	qr/3\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3upd|33),
+	'decrypted query result after update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+is($node->safe_psql('postgres', q{SELECT b, count(*) FROM tbl2 GROUP BY b ORDER BY 2}),
+	q(valB|1
+valA|2),
+	'group by deterministically encrypted column');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl3 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c text ENCRYPTED WITH (column_encryption_key = cek2, algorithm = 'AEAD_AES_192_CBC_HMAC_SHA_384')
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1
+2|valB2|valC2
+3|valB3|valC3),
+	'decrypted query result multiple keys after second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..14eafb8ec9
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Test column master key rotation.  First, we generate CMK1 and a CEK
+# encrypted with it.  Then we add a CMK2 and encrypt the CEK with it
+# as well.  (Recall that a CEK can be associated with multiple CMKs,
+# for this reason.  That's why pg_colenckeydata is split out from
+# pg_colenckey.)  Then we remove CMK1.  We test that we can get
+# decrypted query results at each step.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# generate random bytes
+system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+# encrypt CEK using CMK
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk1filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# create CEK in database
+$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = cmk1, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b) VALUES (1, $1) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with one CMK');
+
+
+# create new CMK
+my $cmk2filename = create_cmk('cmk2');
+
+# encrypt CEK using new CMK
+#
+# (Here, we still have the plaintext of the CEK available from
+# earlier.  In reality, one would decrypt the CEK with the first CMK
+# and then re-encrypt it with the second CMK.)
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk2filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+$cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# add new data record for CEK in database
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} ADD VALUE (column_master_key = cmk2, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with two CMKs');
+
+
+# delete CEK record for first CMK
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} DROP VALUE (column_master_key = cmk1);});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with only new CMK');
+
+
+done_testing();
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (!(!PQparamisencrypted(res2, 0) &&
+		  PQparamisencrypted(res2, 1)))
+	{
+		fprintf(stderr, "wrong results from PQparamisencrypted()\n");
+		return 1;
+	}
+
+	res = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	PGconn	   *conn;
+	int			ret = 0;
+
+	conn = PQconnectdb("");
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		fprintf(stderr, "Connection to database failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (argc < 2 || argv[1] == NULL)
+		return 87;
+	else if (strcmp(argv[1], "test1") == 0)
+		ret = test1(conn);
+	else if (strcmp(argv[1], "test2") == 0)
+		ret = test2(conn);
+	else if (strcmp(argv[1], "test3") == 0)
+		ret = test3(conn);
+	else if (strcmp(argv[1], "test4") == 0)
+		ret = test4(conn);
+	else
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+
+# Test/sample command for libpq cmklookup run scheme
+#
+# This just places the data into temporary files and runs the openssl
+# command on it.  (In practice, this could more simply be written as a
+# shell script, but this way it's more portable.)
+
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $fh, '<:raw', "${tmpdir}/output.tmp" or die $!;
+my $data = '';
+
+while (1) {
+	my $success = read $fh, $data, 100, length($data);
+	die $! if not defined $success;
+	last if not $success;
+}
+
+close $fh;
+
+unlink "${tmpdir}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..d55ddb1ab7 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,7 @@ subdir('subscription')
 subdir('modules')
 
 if ssl.found()
+  subdir('column_encryption')
   subdir('ssl')
 endif
 
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..df47e36280
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,451 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE ROLE regress_enc_user1;
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+CREATE COLUMN MASTER KEY cmk2;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "cek1" already has data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "fail" does not exist
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+ERROR:  column encryption key "notexist" does not exist
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+\d tbl_29f3
+              Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_29f3
+                                        Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+\d tbl_447f
+              Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_447f
+                                        Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  permission denied for column master key cmk1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+ERROR:  permission denied for column encryption key cek1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column master key cmk1 depends on column master key cmk1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ERROR:  column master key "cmk3" already exists in schema "public"
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+ERROR:  column master key "cmkx" does not exist
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ERROR:  column encryption key "cek3" already exists in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+ERROR:  must be owner of column encryption key cek3
+DROP COLUMN MASTER KEY cmk3;  -- fail
+ERROR:  must be owner of column master key cmk3
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ERROR:  column encryption key "cek1" has no data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ERROR:  column master key "fail" does not exist
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+ERROR:  attribute "algorithm" must not be specified
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+ERROR:  column encryption key "fail" does not exist
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf..8dbb4a847a 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -101,6 +103,7 @@ BEGIN
         ('materialized view'), ('foreign table'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
+        ('column encryption key'), ('column encryption key data'), ('column master key'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
         ('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
         ('text search parser'), ('text search dictionary'),
@@ -201,6 +204,24 @@ WARNING:  error for cast,{addr_nsp,zwei},{}: name list length must be exactly 1
 WARNING:  error for cast,{addr_nsp,zwei},{integer}: name list length must be exactly 1
 WARNING:  error for cast,{eins,zwei,drei},{}: name list length must be exactly 1
 WARNING:  error for cast,{eins,zwei,drei},{integer}: name list length must be exactly 1
+WARNING:  error for column encryption key,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins},{}: column master key "eins" does not exist
+WARNING:  error for column master key,{eins},{integer}: column master key "eins" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 WARNING:  error for table constraint,{eins},{}: must specify relation and object name
 WARNING:  error for table constraint,{eins},{integer}: must specify relation and object name
 WARNING:  error for table constraint,{addr_nsp,zwei},{}: relation "addr_nsp" does not exist
@@ -409,6 +430,9 @@ WITH objects (type, name, args) AS (VALUES
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,8 @@ drop cascades to user mapping for regress_addr_user on server integer
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 DROP SCHEMA addr_nsp CASCADE;
 NOTICE:  drop cascades to 14 other objects
 DETAIL:  drop cascades to text search dictionary addr_ts_dict
@@ -547,6 +576,9 @@ WITH objects (classid, objid, objsubid) AS (VALUES
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|NULL
+("(""column master key"",,,)")|("(""column master key"",,)")|NULL
+("(""column encryption key"",,,)")|("(""column encryption key"",,)")|NULL
+("(""column encryption key data"",,,)")|("(""column encryption key data"",,)")|NULL
 -- restore normal output mode
 \a\t
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..226f5e404e 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attusertypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,9 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,13 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
  anyrange                    | anymultirange
-(5 rows)
+(6 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -872,6 +876,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+pg_encrypted_det_eq(pg_encrypted_det,pg_encrypted_det)
+pg_encrypted_det_ne(pg_encrypted_det,pg_encrypted_det)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -75,7 +75,9 @@ ORDER BY t1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | pg_encrypted_det
+ 8244 | pg_encrypted_rnd
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT t1.oid, t1.typname as basetype, t2.typname as arraytype,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 15e015b3d6..577a6d4b2e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 427429975e..57c7d2bec3 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..8d6320015c
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,297 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE ROLE regress_enc_user1;
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk2;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+\d tbl_29f3
+\d+ tbl_29f3
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+
+\d tbl_447f
+\d+ tbl_447f
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+
+
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+
+
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..61828613d9 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -93,6 +95,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
         ('materialized view'), ('foreign table'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
+        ('column encryption key'), ('column encryption key data'), ('column master key'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
         ('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
         ('text search parser'), ('text search dictionary'),
@@ -174,6 +177,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -228,6 +234,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 
 DROP SCHEMA addr_nsp CASCADE;
 
@@ -247,6 +255,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 36ea345f8fa616fd9b40576310e54145aa70c1a1
-- 
2.39.2



Attachments:

  [text/plain] v18-0001-Automatic-client-side-column-level-encryption.patch (462.7K, ../../[email protected]/2-v18-0001-Automatic-client-side-column-level-encryption.patch)
  download | inline diff:
From c73c3f5988aa8c96b7f480916c0eac50ea8b6c1c Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Fri, 10 Mar 2023 08:07:00 +0100
Subject: [PATCH v18] Automatic client-side column-level encryption

This feature enables the automatic encryption and decryption of
particular columns in the client.  The data for those columns then
only ever appears in ciphertext on the server, so it is protected from
DBAs, sysadmins, cloud operators, etc. as well as accidental leakage
to server logs, file-system backups, etc.  The canonical use case for
this feature is storing credit card numbers encrypted, in accordance
with PCI DSS, as well as similar situations involving social security
numbers etc.  One can't do any computations with encrypted values on
the server, but for these use cases, that is not necessary.  This
feature does support deterministic encryption as an alternative to the
default randomized encryption, so in that mode one can do equality
lookups, at the cost of some security.

This functionality also exists in other database products, and the
overall concepts were mostly adopted from there.

(Note: This feature has nothing to do with any on-disk encryption
feature.  Both can exist independently.)

You declare a column as encrypted in a CREATE TABLE statement.  The
column value is encrypted by a symmetric key called the column
encryption key (CEK).  The CEK is a catalog object.  The CEK key
material is in turn encrypted by an asymmetric key called the column
master key (CMK).  The CMK is not stored in the database but somewhere
where the client can get to it, for example in a file or in a key
management system.  When a server sends rows containing encrypted
column values to the client, it first sends the required CMK and CEK
information (new protocol messages), which the client needs to record.
Then, the client can use this information to automatically decrypt the
incoming row data and forward it in plaintext to the application.

For the CMKs, libpq has a new connection parameter "cmklookup" that
specifies via a mini-language where to get the keys.  Right now, you
can use "file" to read it from a file, or "run" to run some program,
which could get it from a KMS.

The general idea would be for an application to have one CMK per area
of secret stuff, for example, for credit card data.  The CMK can be
rotated: each CEK can be represented multiple times in the database,
encrypted by a different CMK.  (The CEK can't be rotated easily, since
that would require reading out all the data from a table/column and
reencrypting it.  We could/should add some custom tooling for that,
but it wouldn't be a routine operation.)

Several encryption algorithms are provided.  The CMK process uses
RSAES_OAEP_SHA_1 or _256.  The CEK process uses
AEAD_AES_*_CBC_HMAC_SHA_* with several strengths.

In the server, the encrypted datums are stored in types called
pg_encrypted_rnd and pg_encrypted_det (for randomized and
deterministic encryption).  These are essentially cousins of bytea.
For the rest of the database system below the protocol handling, there
is nothing special about those.  For example, pg_encrypted_rnd has no
operators at all, pg_encrypted_det has only an equality operator.
pg_attribute has a new column attrealtypid that stores the original
type of the data in the column.  This is only used for providing it to
clients, so that higher-level clients can convert the decrypted value
to their appropriate data types in their environments.

The protocol extensions are guarded by a new protocol extension option
"_pq_.column_encryption".  If this is not set, nothing changes, the
protocol stays the same, and no encryption or decryption happens.

To get automatically encrypted data into the database (as opposed to
reading it out), it is required to use protocol-level prepared
statements (i.e., extended query).  The client must first prepare a
statement, then describe the statement to get parameter metadata,
which indicates which parameters are to be encrypted and how.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this if the
\bind command is used.

Another challenge is that the parse analysis must check which
underlying column a parameter corresponds to.  This is similar to
resorigtbl and resorigcol in the opposite direction.

Discussion: https://www.postgresql.org/message-id/flat/89157929-c2b6-817b-6025-8e4b2d89d88f%40enterprisedb.com
---
 doc/src/sgml/acronyms.sgml                    |  18 +
 doc/src/sgml/catalogs.sgml                    | 317 +++++++
 doc/src/sgml/charset.sgml                     |  10 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 444 +++++++++
 doc/src/sgml/func.sgml                        |  60 ++
 doc/src/sgml/glossary.sgml                    |  26 +
 doc/src/sgml/libpq.sgml                       | 322 +++++++
 doc/src/sgml/protocol.sgml                    | 467 ++++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |  10 +
 .../ref/create_column_encryption_key.sgml     | 173 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  55 +-
 doc/src/sgml/ref/discard.sgml                 |  14 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/grant.sgml                   |  12 +-
 doc/src/sgml/ref/pg_dump.sgml                 |  42 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   8 +
 src/backend/access/common/printtup.c          | 237 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  60 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 288 ++++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  17 +
 src/backend/commands/colenccmds.c             | 439 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/discard.c                |   8 +-
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 197 ++++
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 200 ++++-
 src/backend/parser/parse_param.c              | 157 ++++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  56 ++
 src/backend/utils/adt/acl.c                   | 398 +++++++++
 src/backend/utils/adt/varlena.c               | 107 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  42 +
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/dumputils.c                   |   4 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 377 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  35 +
 src/bin/pg_dump/pg_dump_sort.c                |  14 +
 src/bin/pg_dump/pg_dumpall.c                  |   5 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  72 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 191 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  69 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   4 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |  11 +
 src/include/catalog/pg_colenckey.h            |  46 +
 src/include/catalog/pg_colenckeydata.h        |  46 +
 src/include/catalog/pg_colmasterkey.h         |  47 +
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  10 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               | 103 +++
 src/include/catalog/pg_type.dat               |  13 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  41 +-
 src/include/parser/kwlist.h                   |   3 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   7 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 839 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 671 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 157 +++-
 src/interfaces/libpq/fe-trace.c               |  56 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   1 +
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  23 +
 .../t/001_column_encryption.pl                | 257 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 451 ++++++++++
 src/test/regress/expected/object_address.out  |  35 +
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 297 +++++++
 src/test/regress/sql/object_address.sql       |  11 +
 src/test/regress/sql/type_sanity.sql          |   2 +
 144 files changed, 10393 insertions(+), 84 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/alter_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_master_key.sgml
 create mode 100644 src/backend/commands/colenccmds.c
 create mode 100644 src/common/colenc.c
 create mode 100644 src/include/catalog/pg_colenckey.h
 create mode 100644 src/include/catalog/pg_colenckeydata.h
 create mode 100644 src/include/catalog/pg_colmasterkey.h
 create mode 100644 src/include/commands/colenccmds.h
 create mode 100644 src/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 create mode 100644 src/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/meson.build
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/t/002_cmk_rotation.pl
 create mode 100644 src/test/column_encryption/test_client.c
 create mode 100755 src/test/column_encryption/test_run_decrypt.pl
 create mode 100644 src/test/regress/expected/column_encryption.out
 create mode 100644 src/test/regress/sql/column_encryption.sql

diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 2df6559acc..3a5f2c254c 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -56,6 +56,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CEK</acronym></term>
+    <listitem>
+     <para>
+      Column Encryption Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CIDR</acronym></term>
     <listitem>
@@ -67,6 +76,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CMK</acronym></term>
+    <listitem>
+     <para>
+      Column Master Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CPAN</acronym></term>
     <listitem>
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 746baf5053..54c5199e0f 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -105,6 +105,21 @@ <title>System Catalogs</title>
       <entry>collations (locale information)</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link></entry>
+      <entry>column encryption keys</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link></entry>
+      <entry>column encryption key data</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link></entry>
+      <entry>column master keys</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link></entry>
       <entry>check constraints, unique constraints, primary key constraints, foreign key constraints</entry>
@@ -1360,6 +1375,44 @@ <title><structname>pg_attribute</structname> Columns</title>
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attcek</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, a reference to the column encryption key, else 0.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type of the
+       encrypted data that is reported to the client.  If the column is not
+       encrypted, then 0.  For encrypted columns, the field
+       <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.
+       </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type
+       modifier (analogous to <structfield>atttypmod</structfield>) that is
+       reported to the client.  If the column is not encrypted, then -1.  For
+       encrypted columns, the field <structfield>atttypmod</structfield>)
+       contains the identifier of the encryption algorithm; see <xref
+       linkend="protocol-cek"/> for possible values.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>attinhcount</structfield> <type>int4</type>
@@ -2476,6 +2529,270 @@ <title><structname>pg_collation</structname> Columns</title>
   </para>
  </sect1>
 
+ <sect1 id="catalog-pg-colenckey">
+  <title><structname>pg_colenckey</structname></title>
+
+  <indexterm zone="catalog-pg-colenckey">
+   <primary>pg_colenckey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckey</structname> contains information
+   about the column encryption keys in the database.  The actual key material
+   of the column encryption keys is in the catalog <link
+   linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link>.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column encryption key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column encryption key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colenckeydata">
+  <title><structname>pg_colenckeydata</structname></title>
+
+  <indexterm zone="catalog-pg-colenckeydata">
+   <primary>pg_colenckeydata</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckeydata</structname> contains the key
+   material of column encryption keys.  Each column encryption key object can
+   contain several versions of the key material, each encrypted with a
+   different column master key.  That allows the gradual rotation of the
+   column master keys.  Thus, <literal>(ckdcekid, ckdcmkid)</literal> is a
+   unique key of this table.
+  </para>
+
+  <para>
+   The key material of column encryption keys should never be decrypted inside
+   the database instance.  It is meant to be sent as-is to the client, where
+   it is decrypted using the associated column master key, and then used to
+   encrypt or decrypt column values.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckeydata</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcekid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column encryption key this entry belongs to
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column master key that the key material is encrypted with
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkalg</structfield> <type>int4</type>
+      </para>
+      <para>
+       The encryption algorithm used for encrypting the key material; see
+       <xref linkend="protocol-cmk"/> for possible values.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdencval</structfield> <type>bytea</type>
+      </para>
+      <para>
+       The key material of this column encryption key, encrypted using the
+       referenced column master key
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colmasterkey">
+  <title><structname>pg_colmasterkey</structname></title>
+
+  <indexterm zone="catalog-pg-colmasterkey">
+   <primary>pg_colmasterkey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colmasterkey</structname> contains information
+   about column master keys.  The keys themselves are not stored in the
+   database.  The catalog entry only contains information that is used by
+   clients to locate the keys, for example in a file or in a key management
+   system.
+  </para>
+
+  <table>
+   <title><structname>pg_colmasterkey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column master key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkrealm</structfield> <type>text</type>
+      </para>
+      <para>
+       A <quote>realm</quote> associated with this column master key.  This is
+       a freely chosen string that is used by clients to determine how to look
+       up the key.  A typical configuration would put all CMKs that are looked
+       up in the same way into the same realm.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml
index 3032392b80..f3026fff83 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,16 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When automatic client-side column-level encryption is used, then no
+     encoding conversion is possible.  (The encoding conversion happens on the
+     server, and the server cannot look inside any encrypted column values.)
+     If automatic client-side column-level encryption is enabled for a
+     session, then the server enforces that the client encoding matches the
+     server encoding, and any attempts to change the client encoding will be
+     rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..67fce16872 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <title>Pseudo-Types</title>
 
   </sect1>
 
+  <sect1 id="datatype-encrypted">
+   <title>Types Related to Encryption</title>
+
+   <para>
+    An encrypted column value (see <xref linkend="ddl-column-encryption"/>) is
+    internally stored using the types
+    <type>pg_encrypted_rnd</type> (for randomized encryption) or
+    <type>pg_encrypted_det</type> (for deterministic encryption); see <xref
+    linkend="datatype-encrypted-table"/>.  Most of the database system treats
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support automatic client-side column-level encryption
+    or have disabled it will see the encrypted values in this format.  Clients
+    that support automatic client-side column-level encryption will not see
+    these types in result sets, as the protocol layer will translate them back
+    to the declared underlying type in the table definition.
+   </para>
+
+    <table id="datatype-encrypted-table">
+     <title>Types Related to Encryption</title>
+     <tgroup cols="3">
+      <colspec colname="col1" colwidth="1*"/>
+      <colspec colname="col2" colwidth="3*"/>
+      <colspec colname="col3" colwidth="2*"/>
+      <thead>
+       <row>
+        <entry>Name</entry>
+        <entry>Storage Size</entry>
+        <entry>Description</entry>
+       </row>
+      </thead>
+      <tbody>
+       <row>
+        <entry><type>pg_encrypted_det</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, deterministic encryption</entry>
+       </row>
+       <row>
+        <entry><type>pg_encrypted_rnd</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, randomized encryption</entry>
+       </row>
+      </tbody>
+     </tgroup>
+    </table>
+  </sect1>
  </chapter>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 5179125510..65514e119f 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,440 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   With <firstterm>automatic client-side column-level encryption</firstterm>,
+   columns can be stored encrypted in the database.  The encryption and
+   decryption happens automatically on the client, so that the plaintext value
+   is never seen in the database instance or on the server hosting the
+   database.  The drawback is that most operations, such as function calls or
+   sorting, are not possible on encrypted values.
+  </para>
+
+  <sect2>
+   <title>Using Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   Automatic client-side column-level encryption uses two levels of
+   cryptographic keys.  The actual column value is encrypted using a symmetric
+   algorithm, such as AES, using a <firstterm>column encryption
+   key</firstterm> (<acronym>CEK</acronym>).  The column encryption key is in
+   turn encrypted using an asymmetric algorithm, such as RSA, using a
+   <firstterm>column master key</firstterm> (<acronym>CMK</acronym>).  The
+   encrypted CEK is stored in the database system.  The CMK is not stored in
+   the database system; it is stored on the client or somewhere where the
+   client can access it, such as in a local file or in a key management
+   system.  The database system only records where the CMK is stored and
+   provides this information to the client.  When rows containing encrypted
+   columns are sent to the client, the server first sends any necessary CMK
+   information, followed by any required CEK.  The client then looks up the
+   CMK and uses that to decrypt the CEK.  Then it decrypts incoming row data
+   using the CEK and provides the decrypted row data to the application.
+  </para>
+
+  <para>
+   Here is an example declaring a column as encrypted:
+<programlisting>
+CREATE TABLE customers (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    creditcard_num text <emphasis>ENCRYPTED WITH (column_encryption_key = cek1)</emphasis>
+);
+</programlisting>
+  </para>
+
+  <para>
+   Column encryption supports <firstterm>randomized</firstterm>
+   (also known as <firstterm>probabilistic</firstterm>) and
+   <firstterm>deterministic</firstterm> encryption.  The above example uses
+   randomized encryption, which is the default.  Randomized encryption uses a
+   random initialization vector for each encryption, so that even if the
+   plaintext of two rows is equal, the encrypted values will be different.
+   This prevents someone with direct access to the database server from making
+   computations such as distinct counts on the encrypted values.
+   Deterministic encryption uses a fixed initialization vector.  This reduces
+   security, but it allows equality searches on encrypted values.  The
+   following example declares a column with deterministic encryption:
+<programlisting>
+CREATE TABLE employees (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    ssn text ENCRYPTED WITH (
+        column_encryption_key = cek1, <emphasis>encryption_type = deterministic</emphasis>)
+);
+</programlisting>
+  </para>
+
+  <para>
+   Null values are not encrypted by automatic client-side column-level
+   encryption; null values sent by the client are visible as null values in
+   the database.  If the fact that a value is null needs to be hidden from the
+   server, this information needs to be encoded into a nonnull value in the
+   client somehow.
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Reading and Writing Encrypted Columns</title>
+
+   <para>
+    Reading and writing encrypted columns is meant to be handled automatically
+    by the client library/driver and should be mostly transparent to the
+    application code, if certain prerequisites are fulfilled:
+
+    <itemizedlist>
+     <listitem>
+      <para>
+       The client library needs to support automatic client-side column-level
+       encryption.  Not all client libraries do.  Furthermore, the client
+       library might require that automatic client-side column-level
+       encryption is explicitly enabled at connection time.  See the
+       documentation of the client library for details.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Column master keys and column encryption keys have been set up, and the
+       client library has been configured to be able to look up column master
+       keys from the key store or key management system.
+      </para>
+     </listitem>
+    </itemizedlist>
+   </para>
+
+   <para>
+    Reading from encrypted columns will then work automatically.  For example,
+    using the above example,
+<programlisting>
+SELECT ssn FROM employees WHERE id = 5;
+</programlisting>
+    would return the unencrypted value for the <literal>ssn</literal> column
+    in any rows found.
+   </para>
+
+   <para>
+    Writing to encrypted columns requires that the extended query protocol
+    (protocol-level prepared statements) be used, so that the values to be
+    encrypted are supplied separately from the SQL command.  For example,
+    using, say, psql or libpq, the following would not work:
+<programlisting>
+-- WRONG!
+INSERT INTO employees (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This would leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of client-side column-level encryption.
+    (And even ignoring that, it could not work because the server does not
+    have access to the keys to perform the encryption.)  Note that using
+    server-side prepared statements using the SQL commands
+    <command>PREPARE</command> and <command>EXECUTE</command> is equally
+    incorrect, since that would also leak the parameters provided to
+    <command>EXECUTE</command> to the server.
+   </para>
+
+   <para>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</programlisting>
+    Higher-level client libraries might use the protocol-level prepared
+    statements automatically and thus won't require any code changes.
+   </para>
+
+   <para>
+    <application>psql</application> provides the command
+    <literal>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   The steps to set up automatic client-side column-level encryption for a
+   database are:
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK, for example, using a cryptographic
+      library or toolkit, or a key management system.  Secure access to the
+      key as appropriate, using access control, passwords, etc.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database using the SQL command <xref
+      linkend="sql-create-column-master-key"/>.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the (unencrypted) key material for the CEK in a temporary
+      location.  (It will be encrypted in the next step.  Depending on the
+      available tools, it might be possible and sensible to combine these two
+      steps.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material using the CMK (created earlier).
+      (The unencrypted version of the CEK key material can now be disposed
+      of.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database using the SQL command <xref
+      linkend="sql-create-column-encryption-key"/>.  This command
+      <quote>uploads</quote> the encrypted CEK key material created in the
+      previous step to the database server.  The local copy of the CEK key
+      material can then be removed.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns using the created CEK.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the client library/driver to be able to look up the CMK
+      created earlier.
+     </para>
+    </listitem>
+   </orderedlist>
+
+   Once this is done, values can be written to and read from the encrypted
+   columns in a transparent way.
+  </para>
+
+  <para>
+   Note that these steps should not be run on the database server, but on some
+   client machine.  Neither the CMK nor the unencrypted CEK should ever appear
+   on the database server host.
+  </para>
+
+  <para>
+   The specific details of this setup depend on the desired CMK storage
+   mechanism/key management system as well as the client libraries to be used.
+   The following example uses the <command>openssl</command> command-line tool
+   to set up the keys.
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK and write it to a file:
+<programlisting>
+openssl genpkey -algorithm rsa -out cmk1.pem
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database:
+<programlisting>
+psql ... -c "CREATE COLUMN MASTER KEY cmk1"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</programlisting>
+      (See <xref linkend="protocol-cek-table"/> for required key lengths.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material:
+<programlisting>
+openssl pkeyutl -encrypt -inkey cmk1.pem -pkeyopt rsa_padding_mode:oaep -in cek1.bin -out cek1.bin.enc
+rm cek1.bin
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database:
+<programlisting>
+# convert file contents to hex encoding; this is just one possible way
+cekenchex=$(perl -0ne 'print unpack "H*"' cek1.bin.enc)
+psql ... -c "CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\x${cekenchex}')"
+rm cek1.bin.enc
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns as shown in the examples above.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the libpq for CMK lookup (see also <xref linkend="libpq-connect-cmklookup"/>):
+<programlisting>
+PGCMKLOOKUP="*=file:$PWD/%k.pem"
+export PGCMKLOOKUP
+</programlisting>
+     </para>
+
+     <para>
+      Additionally, libpq requires that the connection parameter <xref
+      linkend="libpq-connect-column-encryption"/> be set in order to activate
+      the automatic client-side column-level encryption functionality.  This
+      should be done in the connection parameters of the application, but an
+      environment variable (<envar>PGCOLUMNENCRYPTION</envar>) is also
+      available.
+     </para>
+    </listitem>
+   </orderedlist>
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Guidance on Using Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use automatic client-side column-level encryption, and what precautions
+    need to be taken to maintain its security.
+   </para>
+
+   <para>
+    In general, column encryption is never a replacement for additional
+    security and encryption techniques such as transport encryption
+    (SSL/TLS), storage encryption, strong access control, and password
+    security.  Column encryption only targets specific use cases and should be
+    used in conjunction with additional security measures.
+   </para>
+
+   <para>
+    A typical use case for column encryption is to encrypt specific values
+    with additional security requirements, for example credit card numbers.
+    This allows you to store that security-sensitive data together with the
+    rest of your data (thus getting various benefits, such as referential
+    integrity, consistent backups), while giving access to that data only to
+    specific clients and preventing accidental leakage on the server side
+    (server logs, file system backups, etc.).
+   </para>
+
+   <para>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation; for
+    <application>libpq</application>, see <xref
+    linkend="libpq-connect-column-encryption"/>).
+   </para>
+
+   <para>
+    Column encryption cannot hide the existence or absence of data, it can
+    only disguise the particular data that is known to exist.  For example,
+    storing a cleartext person name and an encrypted credit card number
+    indicates that the person has a credit card.  That might not reveal too
+    much if the database is for an online store and there is other data nearby
+    that shows that the person has recently made purchases.  But in another
+    example, storing a cleartext person name and an encrypted diagnosis in a
+    medical database probably indicates that the person has a medical issue.
+    Depending on the circumstances, that might not by itself be sufficient
+    security.
+   </para>
+
+   <para>
+    Encryption cannot completely hide the length of values.  The encryption
+    methods will pad values to multiples of the underlying cipher's block size
+    (usually 16 bytes), so some length differences will be unified this way.
+    There is no concern if all values are of the same length, but if there are
+    signficant length differences between valid values and that length
+    information is security-sensitive, then application-specific workarounds
+    such as padding would need to be applied.  How to do that securely is
+    beyond the scope of this manual.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <tip>
+    <para>
+     One might be inclined to think of the client-side column-level encryption
+     feature as a mechanism for application writers and users to protect
+     themselves against an <quote>evil DBA</quote>, but that is not the
+     intended purpose.  Rather, it is (also) a tool for the DBA to control
+     which data they do not want (in plaintext) on the server.
+    </para>
+   </tip>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on is usually
+     subject to government or industry regulations.  This section is not meant
+     to provide complete instructions on how to do this correctly.  Please
+     seek additional advice when engaging in such projects.
+    </para>
+   </note>
+  </sect2>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
@@ -1986,6 +2420,14 @@ <title>Privileges</title>
        server.  Grantees may also create, alter, or drop their own user
        mappings associated with that server.
       </para>
+      <para>
+       For column master keys, allows the creation of column encryption keys
+       using the master key.
+      </para>
+      <para>
+       For column encryption keys, allows the use of the key in the creation
+       of table columns.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2152,6 +2594,8 @@ <title>ACL Privilege Abbreviations</title>
       <entry><literal>USAGE</literal></entry>
       <entry><literal>U</literal></entry>
       <entry>
+       <literal>COLUMN ENCRYPTION KEY</literal>,
+       <literal>COLUMN MASTER KEY</literal>,
        <literal>DOMAIN</literal>,
        <literal>FOREIGN DATA WRAPPER</literal>,
        <literal>FOREIGN SERVER</literal>,
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 9c6107f960..88e2b30f13 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22859,6 +22859,40 @@ <title>Access Privilege Inquiry Functions</title>
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_encryption_key_privilege</primary>
+        </indexterm>
+        <function>has_column_encryption_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cek</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column encryption key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_master_key_privilege</primary>
+        </indexterm>
+        <function>has_column_master_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cmk</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column master key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
@@ -23349,6 +23383,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 7c01a541fe..818038d860 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,32 @@ <title>Glossary</title>
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-column-encryption-key">
+   <glossterm>Column encryption key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt column values when using automatic
+     client-side column-level encryption (<xref
+     linkend="ddl-column-encryption"/>).  Column encryption keys are stored in
+     the database encrypted by another key, the <glossterm
+     linkend="glossary-column-master-key">column master key</glossterm>.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt <glossterm
+     linkend="glossary-column-encryption-key">column encryption
+     keys</glossterm>.  (So the column master key is a <firstterm>key
+     encryption key</firstterm>.)  Column master keys are stored outside the
+     database system, for example in a key management system.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-commit">
    <glossterm>Commit</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3ccd8ff942..26cab10104 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,141 @@ <title>Parameter Key Words</title>
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-connect-column-encryption" xreflabel="column_encryption">
+      <term><literal>column_encryption</literal></term>
+      <listitem>
+       <para>
+        If set to <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, this enables automatic client-side column-level
+        encryption for the connection.  If encrypted columns are queried and
+        this is not enabled, the encrypted values are returned.  See <xref
+        linkend="ddl-column-encryption"/> for more information about this
+        feature.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-cmklookup" xreflabel="cmklookup">
+      <term><literal>cmklookup</literal></term>
+      <listitem>
+       <para>
+        This specifies how libpq should look up column master keys (CMKs) in
+        order to decrypt the column encryption keys (CEKs).
+        The value is a list of <literal>key=value</literal> entries separated
+        by semicolons.  Each key is the name of a key realm, or
+        <literal>*</literal> to match all realms.  The value is a
+        <literal>scheme:data</literal> specification.  The scheme specifies
+        the method to look up the key, the remaining data is specific to the
+        scheme.  Placeholders are replaced in the remaining data as follows:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>%a</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name (see <xref linkend="protocol-cmk-table"/>)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%r</literal></term>
+          <listitem>
+           <para>
+            The realm name
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        Available schemes are:
+        <variablelist>
+         <varlistentry>
+          <term><literal>file</literal></term>
+          <listitem>
+           <para>
+            Load the key material from a file.  The remaining data is the file
+            name.  Use this if the CMKs are kept in a file on the file system.
+           </para>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>run</literal></term>
+          <listitem>
+           <para>
+            Run the specified command to decrypt the CEK.  The remaining data
+            is a shell command.  Use this with key management systems that
+            perform the decryption themselves.  The command must print the
+            decrypted plaintext on the standard output.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        The default value is empty.
+       </para>
+
+       <para>
+        Example:
+<programlisting>
+cmklookup="r1=file:/some/where/secrets/%k.pem;*=file:/else/where/%r/%k.pem"
+</programlisting>
+        This specification says, for keys in realm <quote>r1</quote>, load
+        them from the specified file, replacing <literal>%k</literal> by the
+        key name.  For keys in other realms, load them from the file,
+        replacing realm and key names as specified.
+       </para>
+
+       <para>
+        An example for interacting with a (hypothetical) key management
+        system:
+<programlisting>
+cmklookup="*=run:acmekms decrypt --key %k --alg %a --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2999,32 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            half-byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this half-byte to one to force
+            encryption.  For example, use the C code literal
+            <literal>0x10</literal> to specify text format with forced
+            encryption.  If the array pointer is null then encryption is not
+            forced for any parameter.
+           </para>
+
+           <para>
+            Parameters corresponding to encrypted columns must be passed in
+            text format.  Specifying binary format for such a parameter will
+            result in an error.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter does not
+            correspond to an encrypted column on the server, then the call
+            will fail and the parameter will not be sent.  This can be used
+            for additional security against a compromised server.  (The
+            drawback is that application code then needs to be kept up to date
+            with knowledge about which columns are encrypted rather than
+            letting the server specify this.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3037,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3196,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Sends a request to execute a prepared statement with given
+        parameters, and waits for the result, with support for encrypted columns.
+<synopsis>
+PGresult *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> is like <xref
+        linkend="libpq-PQexecPrepared"/> with additional support for encrypted
+        columns.  The parameter <parameter>paramDesc</parameter> must be a
+        result set obtained from <xref linkend="libpq-PQdescribePrepared"/> on
+        the same prepared statement.
+       </para>
+
+       <para>
+        This function must be used if a statement parameter corresponds to an
+        underlying encrypted column.  In that situation, the prepared
+        statement needs to be described first so that libpq can obtain the
+        necessary key and other information from the server.  When that is
+        done, the parameters corresponding to encrypted columns are
+        automatically encrypted appropriately before being sent to the server.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4084,28 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQfisencrypted">
+     <term><function>PQfisencrypted</function><indexterm><primary>PQfisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given column came from an encrypted
+       column.  Column numbers start at 0.
+<synopsis>
+int PQfisencrypted(const PGresult *res,
+                   int column_number);
+</synopsis>
+      </para>
+
+      <para>
+       Encrypted column values are automatically decrypted, so this function
+       is not necessary to access the column value.  It can be used for extra
+       security to check whether the value was stored encrypted when one
+       thought it should be.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQfsize">
      <term><function>PQfsize</function><indexterm><primary>PQfsize</primary></indexterm></term>
 
@@ -4059,6 +4287,31 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQparamisencrypted">
+     <term><function>PQparamisencrypted</function><indexterm><primary>PQparamisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given parameter is destined for an
+       encrypted column.  Parameter numbers start at 0.
+<synopsis>
+int PQparamisencrypted(const PGresult *res, int param_number);
+</synopsis>
+      </para>
+
+      <para>
+       Values for parameters destined for encrypted columns are automatically
+       encrypted, so this function is not necessary to prepare the parameter
+       value.  It can be used for extra security to check whether the value
+       will be stored encrypted when one thought it should be.  (But see also
+       at <xref linkend="libpq-PQexecPreparedDescribed"/> for another way to do that.)
+       This function is only useful when inspecting the result of <xref
+       linkend="libpq-PQdescribePrepared"/>.  For other types of results it
+       will return false.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQprint">
      <term><function>PQprint</function><indexterm><primary>PQprint</primary></indexterm></term>
 
@@ -4584,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4845,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4902,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4963,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Sends a request to execute a prepared statement with given
+       parameters, without waiting for the result(s), with support for encrypted columns.
+<synopsis>
+int PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> is like <xref
+       linkend="libpq-PQsendQueryPrepared"/> with additional support for encrypted
+       columns.  The parameter <parameter>paramDesc</parameter> must be a
+       result set obtained from <xref linkend="libpq-PQsendDescribePrepared"/> on
+       the same prepared statement.
+      </para>
+
+      <para>
+       This function must be used if a statement parameter corresponds to an
+       underlying encrypted column.  In that situation, the prepared
+       statement needs to be described first so that libpq can obtain the
+       necessary key and other information from the server.  When that is
+       done, the parameters corresponding to encrypted columns are
+       automatically encrypted appropriately before being sent to the server.
+       See also under <xref linkend="libpq-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5052,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8086,26 @@ <title>Environment Variables</title>
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCMKLOOKUP</envar></primary>
+      </indexterm>
+      <envar>PGCMKLOOKUP</envar> behaves the same as the <xref
+      linkend="libpq-connect-cmklookup"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCOLUMNENCRYPTION</envar></primary>
+      </indexterm>
+      <envar>PGCOLUMNENCRYPTION</envar> behaves the same as the <xref
+      linkend="libpq-connect-column-encryption"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 73b7f4432f..c43c3051c3 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,76 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-column-encryption">
+   <title>Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    Automatic client-side column-level encryption is enabled by sending the
+    parameter <literal>_pq_.column_encryption</literal> with a value of
+    <literal>1</literal> in the StartupMessage.  This is a protocol extension
+    that enables a few additional protocol messages and adds additional fields
+    to existing protocol messages.  Client drivers should only activate this
+    protocol extension when requested by the user, for example through a
+    connection parameter.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the
+    messages ColumnMasterKey and ColumnEncryptionKey can appear before
+    RowDescription and ParameterDescription messages.  Clients should collect
+    the information in these messages and keep them for the duration of the
+    connection.  A server is not required to resend the key information for
+    each statement cycle if it was already sent during this connection.  If a
+    server resends a key that the client has already stored (that is, a key
+    having an ID equal to one already stored), the new information should
+    replace the old.  (This could happen, for example, if the key was altered
+    by server-side DDL commands.)
+   </para>
+
+   <para>
+    A client supporting automatic column-level encryption should automatically
+    decrypt the column value fields of DataRow messages corresponding to
+    encrypted columns, and it should automatically encrypt the parameter value
+    fields of Bind messages corresponding to encrypted columns.
+   </para>
+
+   <para>
+    When column encryption is used, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column-level encryption protocol option is enabled.  That way, a client
+    library only needs to support decrypting data sent in binary and does not
+    have to support decoding the text format of the encryption-related types
+    (see <xref linkend="datatype-encrypted"/>).
+   </para>
+
+   <para>
+    When deterministic encryption is used, clients need to take care to
+    represent plaintext to be encrypted in a consistent form.  For example,
+    encrypting an integer represented by the string <literal>100</literal> and
+    an integer represented by the string <literal>+100</literal> would result
+    in two different ciphertexts, thus defeating the main point of
+    deterministic encryption.  This protocol specification requires the
+    plaintext to be in <quote>canonical</quote> form, which is the form that
+    is produced by the server when it outputs a particular value in text
+    format.
+   </para>
+
+   <para>
+    When automatic client-side column-level encryption is enabled, the client
+    encoding must match the server encoding.  This ensures that all values
+    encrypted or decrypted by the client match the server encoding.
+   </para>
+
+   <para>
+    The cryptographic operations used for automatic client-side column-level
+    encryption are described in <xref
+    linkend="protocol-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -3841,6 +3911,16 @@ <title>Message Formats</title>
          The parameter format codes.  Each must presently be
          zero (text) or one (binary).
         </para>
+
+        <para>
+         If the protocol extension <literal>_pq_.column_encryption</literal>
+         is enabled (see <xref linkend="protocol-flow-column-encryption"/>),
+         then the second-least-significant half-byte is set to one if the
+         parameter was encrypted by the client.  (So, for example, to send an
+         encrypted value in binary, the field is set to 0x11 in total.)  This
+         is used by the server to check that a parameter that was required to
+         be encrypted was actually encrypted.
+        </para>
        </listitem>
       </varlistentry>
 
@@ -4061,6 +4141,140 @@ <title>Message Formats</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ColumnEncryptionKey">
+    <term>ColumnEncryptionKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('Y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column encryption key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the master key used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the algorithm used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The length of the following key material.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Byte<replaceable>n</replaceable></term>
+       <listitem>
+        <para>
+         The key material, encrypted with the master key referenced above.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="protocol-message-formats-ColumnMasterKey">
+    <term>ColumnMasterKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-flow-column-encryption"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column master key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The key's realm.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="protocol-message-formats-CommandComplete">
     <term>CommandComplete (B)</term>
     <listitem>
@@ -5164,6 +5378,45 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each parameter:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         This is used as a bit field of flags.  If the parameter is to be
+         encrypted and bit 0x0001 is set, the column underlying the parameter
+         uses deterministic encryption, otherwise randomized encryption.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5552,6 +5805,50 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each field:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         This is used as a bit field of flags.  If the field is encrypted and
+         bit 0x0001 is set, the field uses deterministic encryption, otherwise
+         randomized encryption.
+        </para>
+        <!--
+            This is not really useful here, but it keeps alignment with
+            ParameterDescription.  Future flags might be useful in both
+            places.
+        -->
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7377,6 +7674,176 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-column-encryption-crypto">
+  <title>Automatic Client-side Column-level Encryption Cryptography</title>
+
+  <para>
+   This section describes the cryptographic operations used by the automatic
+   client-side column-level encryption functionality.  A client that supports
+   this functionality needs to implement these operations as specified here in
+   order to be able to interoperate with other clients.
+  </para>
+
+  <para>
+   Column encryption key algorithms and column master key algorithms are
+   identified by integers in the protocol messages and the system catalogs.
+   Additional algorithms may be added to this protocol specification without a
+   change in the protocol version number.  Clients should implement support
+   for all the algorithms specified here.  If a client encounters an algorithm
+   identifier it does not recognize or does not support, it must raise an
+   error.  A suitable error message should be provided to the application or
+   user.
+  </para>
+
+  <sect2 id="protocol-cmk">
+   <title>Column Master Keys</title>
+
+   <para>
+    The currently defined algorithms for column master keys are listed in
+    <xref linkend="protocol-cmk-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC
+       8017</ulink>/PKCS #1)</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect2>
+
+  <sect2 id="protocol-cek">
+   <title>Column Encryption Keys</title>
+
+   <para>
+    The currently defined algorithms for column encryption keys are listed in
+    <xref linkend="protocol-cek-table"/>.
+   </para>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</entry>
+       <entry><literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</entry>
+       <entry><literal>AEAD_AES_192_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_512</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <para>
+    The <quote>associated data</quote> in these algorithms consists of 4
+    bytes: The ASCII letters <literal>P</literal> and <literal>G</literal>
+    (byte values 80 and 71), followed by the version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </para>
+
+   <para>
+    The length of the initialization vector is 16 octets for all CEK algorithm
+    variants.  For randomized encryption, the initialization vector should be
+    (cryptographically strong) random bytes.  For deterministic encryption,
+    the initialization vector is constructed as
+<programlisting>
+SUBSTRING(<replaceable>HMAC</replaceable>(<replaceable>K</replaceable>, <replaceable>P</replaceable>) FOR <replaceable>IVLEN</replaceable>)
+</programlisting>
+    where <replaceable>HMAC</replaceable> is the HMAC function associated with
+    the algorithm, <replaceable>K</replaceable> is the IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </para>
+  </sect2>
+ </sect1>
+
  <sect1 id="protocol-changes">
   <title>Summary of Changes since Protocol 2.0</title>
 
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 54b5f22d6e..a730e5d650 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -8,6 +8,8 @@
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
+<!ENTITY alterColumnEncryptionKey SYSTEM "alter_column_encryption_key.sgml">
+<!ENTITY alterColumnMasterKey SYSTEM "alter_column_master_key.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
 <!ENTITY alterDefaultPrivileges SYSTEM "alter_default_privileges.sgml">
@@ -62,6 +64,8 @@
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
+<!ENTITY createColumnEncryptionKey SYSTEM "create_column_encryption_key.sgml">
+<!ENTITY createColumnMasterKey SYSTEM "create_column_master_key.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
 <!ENTITY createDatabase     SYSTEM "create_database.sgml">
 <!ENTITY createDomain       SYSTEM "create_domain.sgml">
@@ -109,6 +113,8 @@
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
+<!ENTITY dropColumnEncryptionKey SYSTEM "drop_column_encryption_key.sgml">
+<!ENTITY dropColumnMasterKey SYSTEM "drop_column_master_key.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
 <!ENTITY dropDatabase       SYSTEM "drop_database.sgml">
 <!ENTITY dropDomain         SYSTEM "drop_domain.sgml">
diff --git a/doc/src/sgml/ref/alter_column_encryption_key.sgml b/doc/src/sgml/ref/alter_column_encryption_key.sgml
new file mode 100644
index 0000000000..655e1e00d8
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+doc/src/sgml/ref/alter_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-encryption-key">
+ <indexterm zone="sql-alter-column-encryption-key">
+  <primary>ALTER COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>change the definition of a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> ADD VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> DROP VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN ENCRYPTION KEY</command> changes the definition of a
+   column encryption key.
+  </para>
+
+  <para>
+   The first form adds new encrypted key data to a column encryption key,
+   which must be encrypted with a different column master key than the
+   existing key data.  The second form removes a key data entry for a given
+   column master key.  Together, these forms can be used for column master key
+   rotation.
+  </para>
+
+  <para>
+   You must own the column encryption key to use <command>ALTER COLUMN
+   ENCRYPTION KEY</command>.  To alter the owner, you must also be a direct or
+   indirect member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column encryption key's
+   schema.  (These restrictions enforce that altering the owner doesn't do
+   anything you couldn't do by dropping and recreating the column encryption
+   key.  However, a superuser can alter ownership of any column encryption key
+   anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column encryption
+      key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  See <xref
+      linkend="sql-create-column-encryption-key"/> for details
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rotate the master keys used to encrypt a given column encryption key,
+   use a command sequence like this:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (
+    COLUMN_MASTER_KEY = cmk1
+);
+</programlisting>
+  </para>
+
+  <para>
+   To rename the column encryption key <literal>cek1</literal> to
+   <literal>cek2</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column encryption key <literal>cek1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN ENCRYPTION KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/alter_column_master_key.sgml b/doc/src/sgml/ref/alter_column_master_key.sgml
new file mode 100644
index 0000000000..7f0e656ef0
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+doc/src/sgml/ref/alter_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-master-key">
+ <indexterm zone="sql-alter-column-master-key">
+  <primary>ALTER COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN MASTER KEY</refname>
+  <refpurpose>change the definition of a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> ( REALM = <replaceable>realm</replaceable> )
+
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </para>
+
+  <para>
+   You must own the column master key to use <command>ALTER COLUMN MASTER
+   KEY</command>.  To alter the owner, you must also be a direct or indirect
+   member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column master key's schema.
+   (These restrictions enforce that altering the owner doesn't do anything you
+   couldn't do by dropping and recreating the column master key.  However, a
+   superuser can alter ownership of any column master key anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the column master key <literal>cmk1</literal> to
+   <literal>cmk2</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column master key <literal>cmk1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN MASTER KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..1caf9bfa56 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..ebcbf5d00a 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,16 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support automatic client-side
+    column-level encryption or decryption; its input or output data will
+    always be the ciphertext.  This is usually suitable for backups (see also
+    <xref linkend="app-pgdump"/>).  If automatic client-side encryption or
+    decryption is wanted, <command>INSERT</command> and
+    <command>SELECT</command> need to be used instead to write and read the
+    data.
+   </para>
+
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/ref/create_column_encryption_key.sgml b/doc/src/sgml/ref/create_column_encryption_key.sgml
new file mode 100644
index 0000000000..65534fb03f
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,173 @@
+<!--
+doc/src/sgml/ref/create_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-encryption-key">
+ <indexterm zone="sql-create-column-encryption-key">
+  <primary>CREATE COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>define a new column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN ENCRYPTION KEY <replaceable>name</replaceable> WITH VALUES (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    ALGORITHM = <replaceable>algorithm</replaceable>,
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+[ , ... ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN ENCRYPTION KEY</command> defines a new column
+   encryption key.  A column encryption key is used for client-side encryption
+   of table columns that have been defined as encrypted.  The key material of
+   a column encryption key is stored in the database's system catalogs,
+   encrypted (wrapped) by a column master key (which in turn is only
+   accessible to the client, not the database server).
+  </para>
+
+  <para>
+   A column encryption key can be associated with more than one column master
+   key.  To specify that, specify more than one parenthesized definition (see
+   also the examples).
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column encryption key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.  You must have <literal>USAGE</literal> privilege on the
+      column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  Supported algorithms are:
+      <itemizedlist>
+       <listitem>
+        <para><literal>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></para>
+       </listitem>
+      </itemizedlist>
+     </para>
+
+     <para>
+      This is informational only.  The specified value is provided to the
+      client, which may use it for decrypting the column encryption key on the
+      client side.  But a client is also free to ignore this information and
+      figure out how to arrange the decryption in some other way.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+</programlisting>
+  </para>
+
+  <para>
+   To specify more than one associated column master key:
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\xF1F2F2F4...'
+);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_column_master_key.sgml b/doc/src/sgml/ref/create_column_master_key.sgml
new file mode 100644
index 0000000000..6aaa1088d1
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+doc/src/sgml/ref/create_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-master-key">
+ <indexterm zone="sql-create-column-master-key">
+  <primary>CREATE COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN MASTER KEY</refname>
+  <refpurpose>define a new column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN MASTER KEY <replaceable>name</replaceable> [ WITH (
+    [ REALM = <replaceable>realm</replaceable> ]
+) ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN MASTER KEY</command> defines a new column master
+   key.  A column master key is used to encrypt column encryption keys, which
+   are the keys that actually encrypt the column data.  The key material of
+   the column master key is not stored in the database.  The definition of a
+   column master key records information that will allow a client to locate
+   the key material, for example in a file or in a key management system.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column master key.  The name can be
+      schema-qualified.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>realm</replaceable></term>
+
+    <listitem>
+     <para>
+      This is an optional string that can be used to organize column master
+      keys into groups for lookup by clients.  The intent is that all column
+      master keys that are stored in the same system (file system location,
+      key management system, etc.) should be in the same realm.  A client
+      would then be configured to look up all keys in a given realm in a
+      certain way.  See the documentation of the respective client library for
+      further usage instructions.
+     </para>
+
+     <para>
+      The default is the empty string.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+<programlisting>
+CREATE COLUMN MASTER KEY cmk1 (realm = 'myrealm');
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index a03dee4afe..d1549c7f45 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -22,7 +22,7 @@
  <refsynopsisdiv>
 <synopsis>
 CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable> ( [
-  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,47 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables automatic client-side column-level encryption for the column.
+      <replaceable>encryption_options</replaceable> are comma-separated
+      <literal>key=value</literal> specifications.  The following options are
+      available:
+      <variablelist>
+       <varlistentry>
+        <term><literal>column_encryption_key</literal></term>
+        <listitem>
+         <para>
+          Specifies the name of the column encryption key to use.  Specifying
+          this is mandatory.  You must have <literal>USAGE</literal> privilege
+          on the column encryption key.
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>encryption_type</literal></term>
+        <listitem>
+         <para>
+          <literal>randomized</literal> (the default) or <literal>deterministic</literal>
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>algorithm</literal></term>
+        <listitem>
+         <para>
+          The encryption algorithm to use.  The default is
+          <literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -704,6 +745,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</literal></term>
         <listitem>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523ca..6a94706ef7 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@
 
  <refsynopsisdiv>
 <synopsis>
-DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
+DISCARD { ALL | COLUMN ENCRYPTION KEYS | PLANS | SEQUENCES | TEMPORARY | TEMP }
 </synopsis>
  </refsynopsisdiv>
 
@@ -42,6 +42,17 @@ <title>Parameters</title>
 
   <variablelist>
 
+   <varlistentry>
+    <term><literal>COLUMN ENCRYPTION KEYS</literal></term>
+    <listitem>
+     <para>
+      Discards knowledge about which column encryption keys and column master
+      keys have been sent to the client in this session.  (They will
+      subsequently be re-sent as required.)
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>PLANS</literal></term>
     <listitem>
@@ -93,6 +104,7 @@ <title>Parameters</title>
 DISCARD PLANS;
 DISCARD TEMP;
 DISCARD SEQUENCES;
+DISCARD COLUMN ENCRYPTION KEYS;
 </programlisting></para>
     </listitem>
    </varlistentry>
diff --git a/doc/src/sgml/ref/drop_column_encryption_key.sgml b/doc/src/sgml/ref/drop_column_encryption_key.sgml
new file mode 100644
index 0000000000..f2ac1beb08
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_encryption_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-encryption-key">
+ <indexterm zone="sql-drop-column-encryption-key">
+  <primary>DROP COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>remove a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN ENCRYPTION KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN ENCRYPTION KEY</command> removes a previously defined
+   column encryption key.  To be able to drop a column encryption key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column encryption key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column encryption key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column encryption key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column encryption key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN ENCRYPTION KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_column_master_key.sgml b/doc/src/sgml/ref/drop_column_master_key.sgml
new file mode 100644
index 0000000000..fae95e09d1
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_master_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-master-key">
+ <indexterm zone="sql-drop-column-master-key">
+  <primary>DROP COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN MASTER KEY</refname>
+  <refpurpose>remove a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN MASTER KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN MASTER KEY</command> removes a previously defined
+   column master key.  To be able to drop a column master key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column master key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of the column master key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column master key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column master key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN MASTER KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 35bf0332c8..f712f8e9e4 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -46,6 +46,16 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN ENCRYPTION KEY <replaceable>cek_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN MASTER KEY <replaceable>cmk_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON DOMAIN <replaceable>domain_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -513,7 +523,7 @@ <title>Compatibility</title>
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
+    Privileges on databases, tablespaces, schemas, keys, languages, and
     configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 334e4b7fd1..5412ea8666 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -716,6 +716,48 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option turns on the column encryption connection option in
+        <application>libpq</application> (see <xref
+        linkend="libpq-connect-column-encryption"/>).  Column master key
+        lookup must be configured by the user, either through a connection
+        option or an environment setting (see <xref
+        linkend="libpq-connect-cmklookup"/>).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  (But then it is recommended
+        to not do this on the same host as the server, to avoid exposing
+        unencrypted data that is meant to be kept encrypted on the server.)
+        Note that a dump created with this option cannot be restored into a
+        database with column encryption.
+       </para>
+       <!--
+           XXX: The latter would require another pg_dump option to put out
+           INSERT ... \bind ... \g commands.
+       -->
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index e62d05e5ab..4bf60c729f 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -259,6 +259,33 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  Note that a dump created
+        with this option cannot be restored into a database with column
+        encryption.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 7b8ae9fac3..b6115e433e 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4029,6 +4057,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <term><varname>HIDE_COLUMN_ENCRYPTION</varname></term>
+        <listitem>
+        <para>
+         If this variable is set to <literal>true</literal>, column encryption
+         details are not displayed. This is mainly useful for regression
+         tests.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..c898997915 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -36,6 +36,8 @@ <title>SQL Commands</title>
    &abort;
    &alterAggregate;
    &alterCollation;
+   &alterColumnEncryptionKey;
+   &alterColumnMasterKey;
    &alterConversion;
    &alterDatabase;
    &alterDefaultPrivileges;
@@ -90,6 +92,8 @@ <title>SQL Commands</title>
    &createAggregate;
    &createCast;
    &createCollation;
+   &createColumnEncryptionKey;
+   &createColumnMasterKey;
    &createConversion;
    &createDatabase;
    &createDomain;
@@ -137,6 +141,8 @@ <title>SQL Commands</title>
    &dropAggregate;
    &dropCast;
    &dropCollation;
+   &dropColumnEncryptionKey;
+   &dropColumnMasterKey;
    &dropConversion;
    &dropDatabase;
    &dropDomain;
diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index ef818228ac..c5894893eb 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -20,7 +20,9 @@
 
 #include "access/printsimple.h"
 #include "catalog/pg_type.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 
 /*
@@ -46,6 +48,12 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc)
 		pq_sendint16(&buf, attr->attlen);
 		pq_sendint32(&buf, attr->atttypmod);
 		pq_sendint16(&buf, 0);	/* format code */
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&buf, 0);	/* CEK */
+			pq_sendint32(&buf, 0);	/* CEK alg */
+			pq_sendint16(&buf, 0);	/* flags */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 72faeb5dfa..2d627b6f47 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,28 @@
  */
 #include "postgres.h"
 
+#include "access/genam.h"
 #include "access/printtup.h"
+#include "access/skey.h"
+#include "access/table.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
 #include "libpq/libpq.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memdebug.h"
 #include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
 
 
 static void printtup_startup(DestReceiver *self, int operation,
@@ -151,6 +166,166 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 	 */
 }
 
+/*
+ * Send ColumnMasterKey message, unless it's already been sent in this session
+ * for this key.
+ */
+List	   *cmk_sent = NIL;
+
+static void
+cmk_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+}
+
+static void
+MaybeSendColumnMasterKeyMessage(Oid cmkid)
+{
+	HeapTuple	tuple;
+	Form_pg_colmasterkey cmkform;
+	Datum		datum;
+	bool		isnull;
+	StringInfoData buf;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cmk_sent, cmkid))
+		return;
+
+	tuple = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	cmkform = (Form_pg_colmasterkey) GETSTRUCT(tuple);
+
+	pq_beginmessage(&buf, 'y'); /* ColumnMasterKey */
+	pq_sendint32(&buf, cmkform->oid);
+	pq_sendstring(&buf, NameStr(cmkform->cmkname));
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkrealm, &isnull);
+	Assert(!isnull);
+	pq_sendstring(&buf, TextDatumGetCString(datum));
+	pq_endmessage(&buf);
+
+	ReleaseSysCache(tuple);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CMKOID, cmk_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cmk_sent = lappend_oid(cmk_sent, cmkid);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * Send ColumnEncryptionKey message, unless it's already been sent in this
+ * session for this key.
+ */
+List	   *cek_sent = NIL;
+
+static void
+cek_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
+void
+MaybeSendColumnEncryptionKeyMessage(Oid attcek)
+{
+	HeapTuple	tuple;
+	ScanKeyData skey[1];
+	SysScanDesc sd;
+	Relation	rel;
+	bool		found = false;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	if (list_member_oid(cek_sent, attcek))
+		return;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	ScanKeyInit(&skey[0],
+				Anum_pg_colenckeydata_ckdcekid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(attcek));
+	rel = table_open(ColumnEncKeyDataRelationId, AccessShareLock);
+	sd = systable_beginscan(rel, ColumnEncKeyCekidCmkidIndexId, true, NULL, 1, skey);
+
+	while ((tuple = systable_getnext(sd)))
+	{
+		Form_pg_colenckeydata ckdform = (Form_pg_colenckeydata) GETSTRUCT(tuple);
+		Datum		datum;
+		bool		isnull;
+		bytea	   *ba;
+		StringInfoData buf;
+
+		MaybeSendColumnMasterKeyMessage(ckdform->ckdcmkid);
+
+		datum = heap_getattr(tuple, Anum_pg_colenckeydata_ckdencval, RelationGetDescr(rel), &isnull);
+		Assert(!isnull);
+		ba = pg_detoast_datum_packed((bytea *) DatumGetPointer(datum));
+
+		pq_beginmessage(&buf, 'Y'); /* ColumnEncryptionKey */
+		pq_sendint32(&buf, ckdform->ckdcekid);
+		pq_sendint32(&buf, ckdform->ckdcmkid);
+		pq_sendint32(&buf, ckdform->ckdcmkalg);
+		pq_sendint32(&buf, VARSIZE_ANY_EXHDR(ba));
+		pq_sendbytes(&buf, VARDATA_ANY(ba), VARSIZE_ANY_EXHDR(ba));
+		pq_endmessage(&buf);
+
+		found = true;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	systable_endscan(sd);
+	table_close(rel, NoLock);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CEKDATAOID, cek_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cek_sent = lappend_oid(cek_sent, attcek);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+void
+DiscardColumnEncryptionKeys(void)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -167,6 +342,7 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 						  List *targetlist, int16 *formats)
 {
 	int			natts = typeinfo->natts;
+	size_t		sz;
 	int			i;
 	ListCell   *tlist_item = list_head(targetlist);
 
@@ -183,14 +359,18 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 	 * Have to overestimate the size of the column-names, to account for
 	 * character set overhead.
 	 */
-	enlargeStringInfo(buf, (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
-							+ sizeof(Oid)	/* resorigtbl */
-							+ sizeof(AttrNumber)	/* resorigcol */
-							+ sizeof(Oid)	/* atttypid */
-							+ sizeof(int16) /* attlen */
-							+ sizeof(int32) /* attypmod */
-							+ sizeof(int16) /* format */
-							) * natts);
+	sz = (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
+		  + sizeof(Oid)	/* resorigtbl */
+		  + sizeof(AttrNumber)	/* resorigcol */
+		  + sizeof(Oid)	/* atttypid */
+		  + sizeof(int16) /* attlen */
+		  + sizeof(int32) /* attypmod */
+		  + sizeof(int16)); /* format */
+	if (MyProcPort->column_encryption_enabled)
+		sz += (sizeof(int32)		/* attcekid */
+			   + sizeof(int32)		/* attencalg */
+			   + sizeof(int16));	/* flags */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +380,9 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
+		int16		flags = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +414,31 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(atttypid))
+		{
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(resorigtbl), Int16GetDatum(resorigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", resorigcol, resorigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			atttypid = orig_att->attusertypid;
+			atttypmod = orig_att->attusertypmod;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->atttypmod;
+			if (orig_att->atttypid == PG_ENCRYPTED_DETOID)
+				flags |= 0x0001;
+			ReleaseSysCache(tp);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +446,12 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		pq_writeint16(buf, att->attlen);
 		pq_writeint32(buf, atttypmod);
 		pq_writeint16(buf, format);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_writeint32(buf, attcekid);
+			pq_writeint32(buf, attencalg);
+			pq_writeint16(buf, flags);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +485,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 72a2c3d3db..f86ba299c3 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attusertypid != attr2->attusertypid)
+			return false;
+		if (attr1->attusertypmod != attr2->attusertypmod)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attusertypid = 0;
+	att->attusertypmod = -1;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 24bab58499..cc48069932 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index a60107bf94..7b9575635b 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index c4232344aa..a3547c6cae 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_default_acl.h"
@@ -247,6 +249,12 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_SEQUENCE:
 			whole_mask = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			whole_mask = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			whole_mask = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			whole_mask = ACL_ALL_RIGHTS_DATABASE;
 			break;
@@ -473,6 +481,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
 			errormsg = gettext_noop("invalid privilege type %s for sequence");
 			break;
+		case OBJECT_CEK:
+			all_privileges = ACL_ALL_RIGHTS_CEK;
+			errormsg = gettext_noop("invalid privilege type %s for column encryption key");
+			break;
+		case OBJECT_CMK:
+			all_privileges = ACL_ALL_RIGHTS_CMK;
+			errormsg = gettext_noop("invalid privilege type %s for column master key");
+			break;
 		case OBJECT_DATABASE:
 			all_privileges = ACL_ALL_RIGHTS_DATABASE;
 			errormsg = gettext_noop("invalid privilege type %s for database");
@@ -597,6 +613,12 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_SEQUENCE:
 			ExecGrant_Relation(istmt);
 			break;
+		case OBJECT_CEK:
+			ExecGrant_common(istmt, ColumnEncKeyRelationId, ACL_ALL_RIGHTS_CEK, NULL);
+			break;
+		case OBJECT_CMK:
+			ExecGrant_common(istmt, ColumnMasterKeyRelationId, ACL_ALL_RIGHTS_CMK, NULL);
+			break;
 		case OBJECT_DATABASE:
 			ExecGrant_common(istmt, DatabaseRelationId, ACL_ALL_RIGHTS_DATABASE, NULL);
 			break;
@@ -676,6 +698,26 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 				objects = lappend_oid(objects, relOid);
 			}
 			break;
+		case OBJECT_CEK:
+			foreach(cell, objnames)
+			{
+				List	   *cekname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cek_oid(cekname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
+		case OBJECT_CMK:
+			foreach(cell, objnames)
+			{
+				List	   *cmkname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cmk_oid(cmkname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
 		case OBJECT_DATABASE:
 			foreach(cell, objnames)
 			{
@@ -2693,6 +2735,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("permission denied for column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("permission denied for column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -2798,6 +2846,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -2828,6 +2877,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("must be owner of aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("must be owner of column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("must be owner of column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("must be owner of collation %s");
 						break;
@@ -2938,6 +2993,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3019,6 +3075,10 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
+		case OBJECT_CEK:
+			return object_aclmask(ColumnEncKeyRelationId, object_oid, roleid, mask, how);
+		case OBJECT_CMK:
+			return object_aclmask(ColumnMasterKeyRelationId, object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
 		case OBJECT_FUNCTION:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..cab6cfd140 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -30,7 +30,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -153,6 +156,9 @@ static const Oid object_classes[] = {
 	TypeRelationId,				/* OCLASS_TYPE */
 	CastRelationId,				/* OCLASS_CAST */
 	CollationRelationId,		/* OCLASS_COLLATION */
+	ColumnEncKeyRelationId,		/* OCLASS_CEK */
+	ColumnEncKeyDataRelationId,	/* OCLASS_CEKDATA */
+	ColumnMasterKeyRelationId,	/* OCLASS_CMK */
 	ConstraintRelationId,		/* OCLASS_CONSTRAINT */
 	ConversionRelationId,		/* OCLASS_CONVERSION */
 	AttrDefaultRelationId,		/* OCLASS_DEFAULT */
@@ -1493,6 +1499,9 @@ doDeletion(const ObjectAddress *object, int flags)
 
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONVERSION:
 		case OCLASS_LANGUAGE:
 		case OCLASS_OPCLASS:
@@ -2859,6 +2868,15 @@ getObjectClass(const ObjectAddress *object)
 		case CollationRelationId:
 			return OCLASS_COLLATION;
 
+		case ColumnEncKeyRelationId:
+			return OCLASS_CEK;
+
+		case ColumnEncKeyDataRelationId:
+			return OCLASS_CEKDATA;
+
+		case ColumnMasterKeyRelationId:
+			return OCLASS_CMK;
+
 		case ConstraintRelationId:
 			return OCLASS_CONSTRAINT;
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 4f006820b8..df282c796f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypid - 1] = ObjectIdGetDatum(attrs->attusertypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypmod - 1] = Int32GetDatum(attrs->attusertypmod);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attusertypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attusertypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 14e57adee2..00f914bc5f 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -26,6 +26,8 @@
 #include "catalog/dependency.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1997,6 +1999,254 @@ OpfamilyIsVisible(Oid opfid)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4567,6 +4817,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(OpfamilyIsVisible(oid));
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2f688166e1..6c4ab9ac59 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -191,6 +194,48 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_COLLATION,
 		true
 	},
+	{
+		"column encryption key",
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		CEKOID,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		Anum_pg_colenckey_cekowner,
+		Anum_pg_colenckey_cekacl,
+		OBJECT_CEK,
+		true
+	},
+	{
+		"column encryption key data",
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		CEKDATAOID,
+		-1,
+		Anum_pg_colenckeydata_oid,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		-1,
+		false
+	},
+	{
+		"column master key",
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		CMKOID,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		Anum_pg_colmasterkey_cmkowner,
+		Anum_pg_colmasterkey_cmkacl,
+		OBJECT_CMK,
+		true
+	},
 	{
 		"constraint",
 		ConstraintRelationId,
@@ -723,6 +768,18 @@ static const struct object_type_map
 	{
 		"collation", OBJECT_COLLATION
 	},
+	/* OCLASS_CEK */
+	{
+		"column encryption key", OBJECT_CEK
+	},
+	/* OCLASS_CEKDATA */
+	{
+		"column encryption key data", OBJECT_CEKDATA
+	},
+	/* OCLASS_CMK */
+	{
+		"column master key", OBJECT_CMK
+	},
 	/* OCLASS_CONSTRAINT */
 	{
 		"table constraint", OBJECT_TABCONSTRAINT
@@ -1029,6 +1086,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1108,6 +1175,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, castNode(List, object));
+					Oid			cekid;
+					Oid			cmkid;
+
+					cekid = get_cek_oid(cekname, missing_ok);
+					cmkid = get_cmk_oid(cmkname, missing_ok);
+
+					address.classId = ColumnEncKeyDataRelationId;
+					address.objectId = get_cekdata_oid(cekid, cmkid, missing_ok);
+					address.objectSubId = 0;
+				}
+				break;
 			case OBJECT_TRANSFORM:
 				{
 					TypeName   *typename = linitial_node(TypeName, castNode(List, object));
@@ -2311,6 +2393,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2367,6 +2451,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2489,6 +2574,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2575,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3040,6 +3128,92 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CEKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata cekdata;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+
+				cekdata = (Form_pg_colenckeydata) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("column encryption key data of %s for %s"),
+								 getObjectDescription(&(ObjectAddress){ColumnEncKeyRelationId, cekdata->ckdcekid}, false),
+								 getObjectDescription(&(ObjectAddress){ColumnMasterKeyRelationId, cekdata->ckdcmkid}, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4440,6 +4614,18 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "collation");
 			break;
 
+		case OCLASS_CEK:
+			appendStringInfoString(&buffer, "column encryption key");
+			break;
+
+		case OCLASS_CEKDATA:
+			appendStringInfoString(&buffer, "column encryption key data");
+			break;
+
+		case OCLASS_CMK:
+			appendStringInfoString(&buffer, "column master key");
+			break;
+
 		case OCLASS_CONSTRAINT:
 			getConstraintTypeDescription(&buffer, object->objectId,
 										 missing_ok);
@@ -4905,6 +5091,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata form;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckeydata) GETSTRUCT(tup);
+				appendStringInfo(&buffer,
+								 "of %s for %s",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column master key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91..69f6175c60 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -19,6 +19,7 @@ OBJS = \
 	analyze.o \
 	async.o \
 	cluster.o \
+	colenccmds.o \
 	collationcmds.o \
 	comment.o \
 	constraint.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index bea51b3af1..59c23e9ef8 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -22,7 +22,9 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_foreign_data_wrapper.h"
@@ -118,6 +120,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -379,6 +387,8 @@ ExecRenameStmt(RenameStmt *stmt)
 			return RenameType(stmt);
 
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_EVENT_TRIGGER:
@@ -527,6 +537,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -643,6 +655,9 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 			break;
 
 		case OCLASS_CAST:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
@@ -876,6 +891,8 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/colenccmds.c b/src/backend/commands/colenccmds.c
new file mode 100644
index 0000000000..3ada6d5aeb
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,439 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/colenccmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+static void
+parse_cek_attributes(ParseState *pstate, List *definition, Oid *cmkoid_p, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	Assert(cmkoid_p);
+
+	foreach(lc, definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "column_master_key") == 0)
+			defelp = &cmkEl;
+		else if (strcmp(defel->defname, "algorithm") == 0)
+			defelp = &algEl;
+		else if (strcmp(defel->defname, "encrypted_value") == 0)
+			defelp = &encvalEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column encryption key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (cmkEl)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("attribute \"%s\" must be specified",
+						"column_master_key")));
+
+	if (algEl)
+	{
+		char	   *val = defGetString(algEl);
+
+		if (!alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"algorithm")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	if (encvalEl)
+	{
+		if (!encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"encrypted_value")));
+
+		encval = defGetString(encvalEl);
+	}
+	else
+	{
+		if (encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"encrypted_value")));
+	}
+
+	*cmkoid_p = cmkoid;
+	if (alg_p)
+		*alg_p = alg;
+	if (encval_p)
+		*encval_p = encval;
+}
+
+static void
+insert_cekdata_record(Oid cekoid, Oid cmkoid, int alg, char *encval)
+{
+	Oid			cekdataoid;
+	Relation	rel;
+	Datum		values[Natts_pg_colenckeydata] = {0};
+	bool		nulls[Natts_pg_colenckeydata] = {0};
+	HeapTuple	tup;
+	ObjectAddress myself;
+	ObjectAddress other;
+
+	rel = table_open(ColumnEncKeyDataRelationId, RowExclusiveLock);
+
+	cekdataoid = GetNewOidWithIndex(rel, ColumnEncKeyDataOidIndexId, Anum_pg_colenckeydata_oid);
+	values[Anum_pg_colenckeydata_oid - 1] = ObjectIdGetDatum(cekdataoid);
+	values[Anum_pg_colenckeydata_ckdcekid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckeydata_ckdcmkid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colenckeydata_ckdcmkalg - 1] = Int32GetDatum(alg);
+	values[Anum_pg_colenckeydata_ckdencval - 1] = DirectFunctionCall1(byteain, CStringGetDatum(encval));
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyDataRelationId, cekdataoid);
+
+	/* dependency cekdata -> cek */
+	ObjectAddressSet(other, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_AUTO);
+
+	/* dependency cekdata -> cmk */
+	ObjectAddressSet(other, ColumnMasterKeyRelationId, cmkoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_NORMAL);
+
+	table_close(rel, NoLock);
+}
+
+ObjectAddress
+CreateCEK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+	nulls[Anum_pg_colenckey_cekacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOnOwner(ColumnEncKeyRelationId, cekoid, GetUserId());
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnEncKeyRelationId, cekoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt)
+{
+	Oid			cekoid;
+	ObjectAddress address;
+
+	cekoid = get_cek_oid(stmt->cekname, false);
+
+	if (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(stmt->cekname));
+
+	if (stmt->isDrop)
+	{
+		Oid			cmkoid = 0;
+		Oid			cekdataoid;
+		ObjectAddress obj;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, NULL, NULL);
+		cekdataoid = get_cekdata_oid(cekoid, cmkoid, false);
+		ObjectAddressSet(obj, ColumnEncKeyDataRelationId, cekdataoid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+		AclResult	aclresult;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		if (get_cekdata_oid(cekoid, cmkoid, true))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("column encryption key \"%s\" already has data for master key \"%s\"",
+						   NameListToString(stmt->cekname), get_cmk_name(cmkoid, false)));
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	InvokeObjectPostAlterHook(ColumnEncKeyRelationId, cekoid, 0);
+	ObjectAddressSet(address, ColumnEncKeyRelationId, cekoid);
+
+	return address;
+}
+
+ObjectAddress
+CreateCMK(ParseState *pstate, DefineStmt *stmt)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+		realm = defGetString(realmEl);
+	else
+		realm = "";
+
+	cmkoid = GetNewOidWithIndex(rel, ColumnMasterKeyOidIndexId, Anum_pg_colmasterkey_oid);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+	nulls[Anum_pg_colmasterkey_cmkacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	recordDependencyOnOwner(ColumnMasterKeyRelationId, cmkoid, GetUserId());
+
+	ObjectAddressSet(myself, ColumnMasterKeyRelationId, cmkoid);
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d6c6d514f3..9685b7886a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 296dc82d2e..86d22ca065 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/printtup.h"
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "commands/async.h"
@@ -25,7 +26,7 @@
 static void DiscardAll(bool isTopLevel);
 
 /*
- * DISCARD { ALL | SEQUENCES | TEMP | PLANS }
+ * DISCARD
  */
 void
 DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
@@ -36,6 +37,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			DiscardAll(isTopLevel);
 			break;
 
+		case DISCARD_COLUMN_ENCRYPTION_KEYS:
+			DiscardColumnEncryptionKeys();
+			break;
+
 		case DISCARD_PLANS:
 			ResetPlanCache();
 			break;
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	DiscardColumnEncryptionKeys();
 }
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 82bda15889..b4c681005a 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +517,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..4c1628cf7b 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,9 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -1027,6 +1030,9 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_TYPE:
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_CONVERSION:
 		case OCLASS_DEFAULT:
@@ -2056,6 +2062,9 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2139,6 +2148,9 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 42cced9ebe..4b5ac30441 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..93d61c96ad 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,9 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3e2c5f797c..11fa8e741c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,16 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			AclResult	aclresult;
+
+			GetColumnEncryption(colDef->encryption, attr);
+			aclresult = object_aclcheck(ColumnEncKeyRelationId, attr->attcek, GetUserId(), ACL_USAGE);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attr->attcek, false));
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -2569,6 +2582,33 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								attributeName)));
 				def = (ColumnDef *) list_nth(inhSchema, exist_attno - 1);
 
+				/*
+				 * Check encryption parameter.  All parents must have the same
+				 * encryption settings for a column.
+				 */
+				if ((def->encryption && !attribute->attcek) ||
+					(!def->encryption && attribute->attcek))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && attribute->attcek)
+				{
+					/*
+					 * Merging the encryption properties of two encrypted
+					 * parent columns is not yet implemented.  Right now, this
+					 * would confuse the checks of the type etc. below (we
+					 * must check the physical and the real types against each
+					 * other, respectively), which might require a larger
+					 * restructuring.  For now, just give up here.
+					 */
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("multiple inheritance of encrypted columns is not implemented")));
+				}
+
 				/*
 				 * Must have the same type and typmod
 				 */
@@ -2661,6 +2701,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+														attribute->attusertypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -2950,6 +2996,34 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								 errdetail("%s versus %s", def->compression, newdef->compression)));
 				}
 
+				/*
+				 * Check encryption parameter.  All parents and children must
+				 * have the same encryption settings for a column.
+				 */
+				if ((def->encryption && !newdef->encryption) ||
+					(!def->encryption && newdef->encryption))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && newdef->encryption)
+				{
+					FormData_pg_attribute a, newa;
+
+					GetColumnEncryption(def->encryption, &a);
+					GetColumnEncryption(newdef->encryption, &newa);
+
+					if (a.atttypid != newa.atttypid ||
+						a.atttypmod != newa.atttypmod ||
+						a.attcek != newa.attcek)
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" has an encryption specification conflict",
+										attributeName)));
+				}
+
 				/*
 				 * Merge of NOT NULL constraints = OR 'em together
 				 */
@@ -6897,6 +6971,19 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attislocal = colDef->is_local;
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
+	if (colDef->encryption)
+	{
+		GetColumnEncryption(colDef->encryption, &attribute);
+		aclresult = object_aclcheck(ColumnEncKeyRelationId, attribute.attcek, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attribute.attcek, false));
+	}
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attusertypid = 0;
+		attribute.attusertypmod = -1;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12728,6 +12815,9 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
 			case OCLASS_COLLATION:
+			case OCLASS_CEK:
+			case OCLASS_CEKDATA:
+			case OCLASS_CMK:
 			case OCLASS_CONVERSION:
 			case OCLASS_LANGUAGE:
 			case OCLASS_LARGEOBJECT:
@@ -19328,3 +19418,110 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	List	   *cek = NULL;
+	Oid			cekoid;
+	bool		encdet = false;
+	int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+	foreach(lc, coldefencryption)
+	{
+		DefElem    *el = lfirst_node(DefElem, lc);
+
+		if (strcmp(el->defname, "column_encryption_key") == 0)
+			cek = defGetQualifiedName(el);
+		else if (strcmp(el->defname, "encryption_type") == 0)
+		{
+			char	   *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+			if (strcmp(val, "deterministic") == 0)
+				encdet = true;
+			else if (strcmp(val, "randomized") == 0)
+				encdet = false;
+			else
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption type: %s", val));
+		}
+		else if (strcmp(el->defname, "algorithm") == 0)
+		{
+			char	   *val = strVal(el->arg);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized encryption algorithm: %s", val));
+		}
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized column encryption parameter: %s", el->defname));
+	}
+
+	if (!cek)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+				errmsg("column encryption key must be specified"));
+
+	cekoid = get_cek_oid(cek, false);
+
+	attr->attcek = cekoid;
+	attr->attusertypid = attr->atttypid;
+	attr->attusertypmod = attr->atttypmod;
+
+	/* override physical type */
+	if (encdet)
+		attr->atttypid = PG_ENCRYPTED_DETOID;
+	else
+		attr->atttypid = PG_ENCRYPTED_RNDOID;
+	get_typlenbyvalalign(attr->atttypid,
+						 &attr->attlen, &attr->attbyval, &attr->attalign);
+	attr->attstorage = get_typstorage(attr->atttypid);
+	attr->attcollation = InvalidOid;
+
+	attr->atttypmod = alg;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	List	   *result;
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	char	   *nspname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attr->attcek));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attr->attcek);
+
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+	nspname = get_namespace_name(form->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(form->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->atttypmod))),
+									-1));
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index f0f2e07655..c65f6a4388 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -25,6 +25,7 @@
 #include "access/xlogprefetcher.h"
 #include "catalog/pg_authid.h"
 #include "common/string.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "postmaster/postmaster.h"
@@ -706,7 +707,11 @@ check_client_encoding(char **newval, void **extra, GucSource source)
 	 */
 	if (PrepareClientEncoding(encoding) < 0)
 	{
-		if (IsTransactionState())
+		if (MyProcPort->column_encryption_enabled)
+			GUC_check_errdetail("Conversion between %s and %s is not possible when column encryption is enabled.",
+								canonical_name,
+								GetDatabaseEncodingName());
+		else if (IsTransactionState())
 		{
 			/* Must be a genuine no-such-conversion problem */
 			GUC_check_errcode(ERRCODE_FEATURE_NOT_SUPPORTED);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index ff98c773f5..171dca3803 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index dc8415a693..c81bc15128 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3983,6 +3983,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(coldef->compression))
 					return true;
+				if (WALK(coldef->encryption))
+					return true;
 				if (WALK(coldef->raw_default))
 					return true;
 				if (WALK(coldef->collClause))
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a0138382a1..a6039878fa 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,6 +420,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <list>	parse_toplevel stmtmulti routine_body_stmt_list
 				OptTableElementList TableElementList OptInherit definition
+				list_of_definitions
 				OptTypedTableElementList TypedTableElementList
 				reloptions opt_reloptions
 				OptWith opt_definition func_args func_args_list
@@ -592,6 +594,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableConstraint TableLikeClause
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression column_storage opt_column_storage
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_match
@@ -690,8 +693,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
 	DOUBLE_P DROP
 
-	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
-	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EACH ELSE ENABLE_P ENCODING ENCRYPTION ENCRYPTED END_P ENUM_P ESCAPE
+	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -708,13 +711,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	JOIN
 
-	KEY
+	KEY KEYS
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
-	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+	MAPPING MASTER MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
 
 	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -1988,7 +1993,7 @@ CheckPointStmt:
 
 /*****************************************************************************
  *
- * DISCARD { ALL | TEMP | PLANS | SEQUENCES }
+ * DISCARD
  *
  *****************************************************************************/
 
@@ -2014,6 +2019,13 @@ DiscardStmt:
 					n->target = DISCARD_TEMP;
 					$$ = (Node *) n;
 				}
+			| DISCARD COLUMN ENCRYPTION KEYS
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+
+					n->target = DISCARD_COLUMN_ENCRYPTION_KEYS;
+					$$ = (Node *) n;
+				}
 			| DISCARD PLANS
 				{
 					DiscardStmt *n = makeNode(DiscardStmt);
@@ -3700,14 +3712,15 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_storage opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_encryption opt_column_storage opt_column_compression create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 
 					n->colname = $1;
 					n->typeName = $2;
-					n->storage_name = $3;
-					n->compression = $4;
+					n->encryption = $3;
+					n->storage_name = $4;
+					n->compression = $5;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3716,8 +3729,8 @@ columnDef:	ColId Typename opt_column_storage opt_column_compression create_gener
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $5;
-					SplitColQualList($6, &n->constraints, &n->collClause,
+					n->fdwoptions = $6;
+					SplitColQualList($7, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *) n;
@@ -3774,6 +3787,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4052,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6289,33 @@ DefineStmt:
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+			| CREATE COLUMN ENCRYPTION KEY any_name WITH VALUES list_of_definitions
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CEK;
+					n->defnames = $5;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name WITH definition
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = $7;
+					$$ = (Node *) n;
+				}
 		;
 
 definition: '(' def_list ')'						{ $$ = $2; }
@@ -6289,6 +6335,10 @@ def_elem:	ColLabel '=' def_arg
 				}
 		;
 
+list_of_definitions: definition						{ $$ = list_make1($1); }
+			| list_of_definitions ',' definition	{ $$ = lappend($1, $3); }
+		;
+
 /* Note: any simple identifier will be returned as a type name! */
 def_arg:	func_type						{ $$ = (Node *) $1; }
 			| reserved_keyword				{ $$ = (Node *) makeString(pstrdup($1)); }
@@ -6800,6 +6850,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -7611,6 +7663,24 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| COLUMN ENCRYPTION KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CEK;
+					n->objs = $4;
+					$$ = n;
+				}
+			| COLUMN MASTER KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CMK;
+					n->objs = $4;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9140,6 +9210,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9817,6 +9907,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10148,6 +10258,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11304,6 +11432,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_name ADD_P VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = false;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN ENCRYPTION KEY any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16965,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16840,6 +17015,7 @@ unreserved_keyword:
 			| INVOKER
 			| ISOLATION
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -16854,6 +17030,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17514,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17404,6 +17582,7 @@ bare_label_keyword:
 			| ISOLATION
 			| JOIN
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -17425,6 +17604,7 @@ bare_label_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index 2240284f21..a8a1855aa0 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,159 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			Expr	   *expr = te->expr;
+
+			/*
+			 * If it's a RelabelType, look inside.  (For encrypted columns,
+			 * this would typically be a typmod adjustment.)
+			 */
+			if (IsA(expr, RelabelType))
+				expr = castNode(RelabelType, expr)->arg;
+
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(expr, Param))
+			{
+				Param	   *p = (Param *) expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(expr, Var))
+			{
+				Var		   *v = (Var *) expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell   *lc;
+
+					foreach(lc, srcrte->values_lists)
+					{
+						List	   *values_list = lfirst_node(List, lc);
+						Expr	   *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, RelabelType))
+							value = castNode(RelabelType, value)->arg;
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach(lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f9218f48aa..8d902292cd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1035,8 +1035,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+												attribute->attusertypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2552327d90..79ec9c4d1f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2210,12 +2210,27 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 									valptr),
 							 errhint("Valid values are: \"false\", 0, \"true\", 1, \"database\".")));
 			}
+			else if (strcmp(nameptr, "_pq_.column_encryption") == 0)
+			{
+				/*
+				 * Right now, the only accepted value is "1".  This gives room
+				 * to expand this into a version number, for example.
+				 */
+				if (strcmp(valptr, "1") == 0)
+					port->column_encryption_enabled = true;
+				else
+					ereport(FATAL,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for parameter \"%s\": \"%s\"",
+									"column_encryption",
+									valptr),
+							 errhint("Valid values are: 1.")));
+			}
 			else if (strncmp(nameptr, "_pq_.", 5) == 0)
 			{
 				/*
 				 * Any option beginning with _pq_. is reserved for use as a
-				 * protocol-level option, but at present no such options are
-				 * defined.
+				 * protocol-level option.
 				 */
 				unrecognized_protocol_options =
 					lappend(unrecognized_protocol_options, pstrdup(nameptr));
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index cab709b07b..b577557384 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -71,6 +72,7 @@
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
 
@@ -1815,6 +1817,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (type_is_encrypted(ptype))
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter $%d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2560,6 +2572,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2618,11 +2632,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	for (int i = 0; i < psrc->num_params; i++)
 	{
 		Oid			ptype = psrc->param_types[i];
+		Oid			pcekid = InvalidOid;
+		int			pcekalg = 0;
+		int16		pflags = 0;
+
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(ptype))
+		{
+			Oid			porigtbl = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("parameter $%d corresponds to an encrypted column, but an underlying table and column could not be determined", i + 1)));
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(porigtbl), Int16GetDatum(porigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", porigcol, porigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			ptype = orig_att->attusertypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x0001;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&row_description_buf, (int) pcekid);
+			pq_sendint32(&row_description_buf, pcekalg);
+			pq_sendint16(&row_description_buf, pflags);
+		}
 	}
 	pq_endmessage_reuse(&row_description_buf);
 
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index c7d9d96b45..180f86cb79 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -30,6 +30,7 @@
 #include "commands/alter.h"
 #include "commands/async.h"
 #include "commands/cluster.h"
+#include "commands/colenccmds.h"
 #include "commands/collationcmds.h"
 #include "commands/comment.h"
 #include "commands/conversioncmds.h"
@@ -137,6 +138,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,14 @@ ProcessUtilitySlow(ParseState *pstate,
 															stmt->definition,
 															&secondaryObject);
 							break;
+						case OBJECT_CEK:
+							Assert(stmt->args == NIL);
+							address = CreateCEK(pstate, stmt);
+							break;
+						case OBJECT_CMK:
+							Assert(stmt->args == NIL);
+							address = CreateCMK(pstate, stmt);
+							break;
 						case OBJECT_COLLATION:
 							Assert(stmt->args == NIL);
 							address = DefineCollation(pstate,
@@ -1903,6 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,12 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_COLUMN:
 			tag = CMDTAG_ALTER_TABLE;
 			break;
+		case OBJECT_CEK:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+		case OBJECT_CMK:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
 		case OBJECT_CONVERSION:
 			tag = CMDTAG_ALTER_CONVERSION;
 			break;
@@ -2640,6 +2665,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_DROP_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_DROP_COLUMN_MASTER_KEY;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2760,6 +2791,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_COLLATION:
 					tag = CMDTAG_CREATE_COLLATION;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_CREATE_COLUMN_MASTER_KEY;
+					break;
 				case OBJECT_ACCESS_METHOD:
 					tag = CMDTAG_CREATE_ACCESS_METHOD;
 					break;
@@ -2917,6 +2954,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_ALL:
 					tag = CMDTAG_DISCARD_ALL;
 					break;
+				case DISCARD_COLUMN_ENCRYPTION_KEYS:
+					tag = CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS;
+					break;
 				case DISCARD_PLANS:
 					tag = CMDTAG_DISCARD_PLANS;
 					break;
@@ -3063,6 +3103,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3736,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 8f7522d103..6aeb06f8fd 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -22,6 +22,8 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_foreign_data_wrapper.h"
 #include "catalog/pg_foreign_server.h"
@@ -101,6 +103,10 @@ static AclMode convert_table_priv_string(text *priv_type_text);
 static AclMode convert_sequence_priv_string(text *priv_type_text);
 static AttrNumber convert_column_name(Oid tableoid, text *column);
 static AclMode convert_column_priv_string(text *priv_type_text);
+static Oid	convert_column_encryption_key_name(text *cekname);
+static AclMode convert_column_encryption_key_priv_string(text *priv_type_text);
+static Oid	convert_column_master_key_name(text *cmkname);
+static AclMode convert_column_master_key_priv_string(text *priv_type_text);
 static Oid	convert_database_name(text *databasename);
 static AclMode convert_database_priv_string(text *priv_type_text);
 static Oid	convert_foreign_data_wrapper_name(text *fdwname);
@@ -800,6 +806,14 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			/* for backwards compatibility, grant some rights by default */
 			world_default = ACL_CREATE_TEMP | ACL_CONNECT;
@@ -911,6 +925,12 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 's':
 			objtype = OBJECT_SEQUENCE;
 			break;
+		case 'Y':
+			objtype = OBJECT_CEK;
+			break;
+		case 'y':
+			objtype = OBJECT_CMK;
+			break;
 		case 'd':
 			objtype = OBJECT_DATABASE;
 			break;
@@ -2915,6 +2935,384 @@ convert_column_priv_string(text *priv_type_text)
 }
 
 
+/*
+ * has_column_encryption_key_privilege variants
+ *		These are all named "has_column_encryption_key_privilege" at the SQL level.
+ *		They take various combinations of column encryption key name,
+ *		cek OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_encryption_key_privilege_name_name
+ *		Check user privileges on a column encryption key given
+ *		name username, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name
+ *		Check user privileges on a column encryption key given
+ *		text cekname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cekname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name_id
+ *		Check user privileges on a column encryption key given
+ *		name usename, column encryption key oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id
+ *		Check user privileges on a column encryption key given
+ *		column encryption key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cekid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_name
+ *		Check user privileges on a column encryption key given
+ *		roleid, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_id
+ *		Check user privileges on a column encryption key given
+ *		roleid, cek oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_encryption_key_privilege family.
+ */
+
+/*
+ * Given a CEK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_encryption_key_name(text *cekname)
+{
+	return get_cek_oid(textToQualifiedNameList(cekname), false);
+}
+
+/*
+ * convert_column_encryption_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_encryption_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_encryption_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_encryption_key_priv_map);
+}
+
+
+/*
+ * has_column_master_key_privilege variants
+ *		These are all named "has_column_master_key_privilege" at the SQL level.
+ *		They take various combinations of column master key name,
+ *		cmk OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_master_key_privilege_name_name
+ *		Check user privileges on a column master key given
+ *		name username, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name
+ *		Check user privileges on a column master key given
+ *		text cmkname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cmkname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name_id
+ *		Check user privileges on a column master key given
+ *		name usename, column master key oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id
+ *		Check user privileges on a column master key given
+ *		column master key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cmkid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_name
+ *		Check user privileges on a column master key given
+ *		roleid, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_id
+ *		Check user privileges on a column master key given
+ *		roleid, cmk oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_master_key_privilege family.
+ */
+
+/*
+ * Given a CMK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_master_key_name(text *cmkname)
+{
+	return get_cmk_oid(textToQualifiedNameList(cmkname), false);
+}
+
+/*
+ * convert_column_master_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_master_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_master_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_master_key_priv_map);
+}
+
+
 /*
  * has_database_privilege variants
  *		These are all named "has_database_privilege" at the SQL level.
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 5778e3f0ef..dd21fcf59e 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -679,6 +679,113 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	Node	   *escontext = fcinfo->context;
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c07382051d..7ad159110f 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -24,7 +24,10 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_namespace.h"
@@ -2658,6 +2661,25 @@ type_is_multirange(Oid typid)
 	return (get_typtype(typid) == TYPTYPE_MULTIRANGE);
 }
 
+bool
+type_is_encrypted(Oid typid)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+		bool		result;
+
+		result = (typtup->typcategory == TYPCATEGORY_ENCRYPTED);
+		ReleaseSysCache(tp);
+		return result;
+	}
+	else
+		return false;
+}
+
 /*
  * get_type_category_preferred
  *
@@ -3683,3 +3705,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+char *
+get_cek_name(Oid cekid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cekname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+		return NULL;
+	}
+
+	cekname = pstrdup(NameStr(((Form_pg_colenckey) GETSTRUCT(tup))->cekname));
+
+	ReleaseSysCache(tup);
+
+	return cekname;
+}
+
+Oid
+get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok)
+{
+	Oid			cekdataid;
+
+	cekdataid = GetSysCacheOid2(CEKDATACEKCMK, Anum_pg_colenckeydata_oid,
+								ObjectIdGetDatum(cekid),
+								ObjectIdGetDatum(cmkid));
+	if (!OidIsValid(cekdataid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" has no data for master key \"%s\"",
+						get_cek_name(cekid, false), get_cmk_name(cmkid, false))));
+
+	return cekdataid;
+}
+
+char *
+get_cmk_name(Oid cmkid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cmkname;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+		return NULL;
+	}
+
+	cmkname = pstrdup(NameStr(((Form_pg_colmasterkey) GETSTRUCT(tup))->cmkname));
+
+	ReleaseSysCache(tup);
+
+	return cmkname;
+}
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 77c2ba3f8f..d40af13efe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 94abede512..eb432260da 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -219,6 +222,32 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAMENSP] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameNspIndexId,
+		KEY(Anum_pg_colenckey_cekname,
+			Anum_pg_colenckey_ceknamespace),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +262,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAMENSP] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameNspIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname,
+			Anum_pg_colmasterkey_cmknamespace),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 033647011b..4b6be68f27 100644
--- a/src/backend/utils/mb/mbutils.c
+++ b/src/backend/utils/mb/mbutils.c
@@ -36,7 +36,9 @@
 
 #include "access/xact.h"
 #include "catalog/namespace.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
@@ -130,6 +132,12 @@ PrepareClientEncoding(int encoding)
 		encoding == PG_SQL_ASCII)
 		return 0;
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	if (IsTransactionState())
 	{
 		/*
@@ -237,6 +245,12 @@ SetClientEncoding(int encoding)
 		return 0;
 	}
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	/*
 	 * Search the cache for the entry previously prepared by
 	 * PrepareClientEncoding; if there isn't one, we lose.  While at it,
@@ -297,7 +311,9 @@ InitializeClientEncoding(void)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("conversion between %s and %s is not supported",
 						pg_enc2name_tbl[pending_client_encoding].name,
-						GetDatabaseEncodingName())));
+						GetDatabaseEncodingName()),
+				 (MyProcPort->column_encryption_enabled) ?
+				 errdetail("Encoding conversion is not possible when column encryption is enabled.") : 0));
 	}
 
 	/*
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index a2b74901e4..f35df16080 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -201,6 +203,12 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading user-defined collations");
 	(void) getCollations(fout, &numCollations);
 
+	pg_log_info("reading column master keys");
+	getColumnMasterKeys(fout);
+
+	pg_log_info("reading column encryption keys");
+	getColumnEncryptionKeys(fout);
+
 	pg_log_info("reading user-defined conversions");
 	getConversions(fout, &numConversions);
 
@@ -876,6 +884,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 079693585c..eeed0db211 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -484,6 +484,10 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('U', "USAGE");
 	}
+	else if (strcmp(type, "COLUMN ENCRYPTION KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "COLUMN MASTER KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "DATABASE") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,7 @@ typedef struct _connParams
 	char	   *pghost;
 	char	   *username;
 	trivalue	promptPassword;
+	int			column_encryption;
 	/* If not NULL, this overrides the dbname obtained from command line */
 	/* (but *only* the DB name, not anything else in the connstring) */
 	char	   *override_dbname;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 61ebb8fe85..bc303550fd 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3396,6 +3396,8 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 
 	/* objects that don't require special decoration */
 	if (strcmp(type, "COLLATION") == 0 ||
+		strcmp(type, "COLUMN ENCRYPTION KEY") == 0 ||
+		strcmp(type, "COLUMN MASTER KEY") == 0 ||
 		strcmp(type, "CONVERSION") == 0 ||
 		strcmp(type, "DOMAIN") == 0 ||
 		strcmp(type, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 100644
--- a/src/bin/pg_dump/pg_backup_db.c
+++ b/src/bin/pg_dump/pg_backup_db.c
@@ -133,8 +133,8 @@ ConnectDatabase(Archive *AHX,
 	 */
 	do
 	{
-		const char *keywords[8];
-		const char *values[8];
+		const char *keywords[9];
+		const char *values[9];
 		int			i = 0;
 
 		/*
@@ -159,6 +159,11 @@ ConnectDatabase(Archive *AHX,
 		}
 		keywords[i] = "fallback_application_name";
 		values[i++] = progname;
+		if (cparams->column_encryption)
+		{
+			keywords[i] = "column_encryption";
+			values[i++] = "1";
+		}
 		keywords[i] = NULL;
 		values[i++] = NULL;
 		Assert(i <= lengthof(keywords));
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4217908f84..f57bccee23 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,8 @@ static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
 static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
 static void dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo);
 static void dumpCollation(Archive *fout, const CollInfo *collinfo);
+static void dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo);
+static void dumpColumnMasterKey(Archive *fout, const CmkInfo *cekinfo);
 static void dumpConversion(Archive *fout, const ConvInfo *convinfo);
 static void dumpRule(Archive *fout, const RuleInfo *rinfo);
 static void dumpAgg(Archive *fout, const AggInfo *agginfo);
@@ -393,6 +396,7 @@ main(int argc, char **argv)
 		{"attribute-inserts", no_argument, &dopt.column_inserts, 1},
 		{"binary-upgrade", no_argument, &dopt.binary_upgrade, 1},
 		{"column-inserts", no_argument, &dopt.column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &dopt.cparams.column_encryption, 1},
 		{"disable-dollar-quoting", no_argument, &dopt.disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &dopt.disable_triggers, 1},
 		{"enable-row-security", no_argument, &dopt.enable_row_security, 1},
@@ -685,6 +689,9 @@ main(int argc, char **argv)
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
 	 */
+	if (dopt.cparams.column_encryption && dopt.dump_inserts == 0)
+		pg_fatal("option --decrypt-encrypted-columns requires option --inserts, --rows-per-insert, or --column-inserts");
+
 	if (dopt.do_nothing && dopt.dump_inserts == 0)
 		pg_fatal("option --on-conflict-do-nothing requires option --inserts, --rows-per-insert, or --column-inserts");
 
@@ -1056,6 +1063,7 @@ help(const char *progname)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security (dump only content user has\n"
@@ -5587,6 +5595,164 @@ getCollations(Archive *fout, int *numCollations)
 	return collinfo;
 }
 
+/*
+ * getColumnEncryptionKeys
+ *	  get information about column encryption keys
+ */
+void
+getColumnEncryptionKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CekInfo	   *cekinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cekname;
+	int			i_ceknamespace;
+	int			i_cekowner;
+	int			i_cekacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, cek.cekowner, cek.cekacl, acldefault('Y', cek.cekowner) AS acldefault\n"
+					  "FROM pg_colenckey cek");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cekname = PQfnumber(res, "cekname");
+	i_ceknamespace = PQfnumber(res, "ceknamespace");
+	i_cekowner = PQfnumber(res, "cekowner");
+	i_cekacl = PQfnumber(res, "cekacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cekinfo = pg_malloc(ntups * sizeof(CekInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		PGresult	   *res2;
+		int				ntups2;
+
+		cekinfo[i].dobj.objType = DO_CEK;
+		cekinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cekinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cekinfo[i].dobj);
+		cekinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cekname));
+		cekinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cekacl));
+		cekinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cekinfo[i].dacl.privtype = 0;
+		cekinfo[i].dacl.initprivs = NULL;
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, ckdcmkalg, ckdencval\n"
+						  "FROM pg_catalog.pg_colenckeydata\n"
+						  "WHERE ckdcekid = %u", cekinfo[i].dobj.catId.oid);
+		res2 = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		ntups2 = PQntuples(res2);
+		cekinfo[i].numdata = ntups2;
+		cekinfo[i].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			cekinfo[i].cekcmkalgs[j] = atoi(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkalg")));
+			cekinfo[i].cekencvals[j] = pg_strdup(PQgetvalue(res2, j, PQfnumber(res2, "ckdencval")));
+		}
+		PQclear(res2);
+
+		selectDumpableObject(&(cekinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cekacl))
+			cekinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * getColumnMasterKeys
+ *	  get information about column master keys
+ */
+void
+getColumnMasterKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CmkInfo	   *cmkinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cmkname;
+	int			i_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+	int			i_cmkacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, cmk.cmkowner, cmk.cmkrealm, cmk.cmkacl, acldefault('y', cmk.cmkowner) AS acldefault\n"
+					  "FROM pg_colmasterkey cmk");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cmkname = PQfnumber(res, "cmkname");
+	i_cmknamespace = PQfnumber(res, "cmknamespace");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+	i_cmkacl = PQfnumber(res, "cmkacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	cmkinfo = pg_malloc(ntups * sizeof(CmkInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		cmkinfo[i].dobj.objType = DO_CMK;
+		cmkinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cmkinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cmkinfo[i].dobj);
+		cmkinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cmkname));
+		cmkinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		cmkinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cmkacl));
+		cmkinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cmkinfo[i].dacl.privtype = 0;
+		cmkinfo[i].dacl.initprivs = NULL;
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cmkacl))
+			cmkinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8202,6 +8368,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8261,8 +8430,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
+	appendPQExpBuffer(q, "SELECT\n"
 						 "a.attrelid,\n"
 						 "a.attnum,\n"
 						 "a.attname,\n"
@@ -8275,7 +8443,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attlen,\n"
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
+						 "pg_catalog.format_type(%s) AS atttypname,\n"
 						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
@@ -8284,7 +8452,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "' ' || pg_catalog.quote_literal(option_value) "
 						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
 						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+						 "), E',\n    ') AS attfdwoptions,\n",
+						 fout->remoteVersion >= 160000 ?
+						 "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END, CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END" :
+						 "a.atttypid, a.atttypmod");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -8310,10 +8481,23 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	if (fout->remoteVersion >= 120000)
 		appendPQExpBufferStr(q,
-							 "a.attgenerated\n");
+							 "a.attgenerated,\n");
+	else
+		appendPQExpBufferStr(q,
+							 "'' AS attgenerated,\n");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBuffer(q,
+						  "a.attcek,\n"
+						  "CASE a.atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "CASE WHEN a.atttypid IN (%u, %u) THEN a.atttypmod END AS attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID,
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcek,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
@@ -8338,6 +8522,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	i_attencdet = PQfnumber(res, "attencdet");
+	i_attencalg = PQfnumber(res, "attencalg");
 	i_attisdropped = PQfnumber(res, "attisdropped");
 	i_attlen = PQfnumber(res, "attlen");
 	i_attalign = PQfnumber(res, "attalign");
@@ -8398,6 +8585,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		tbinfo->typstorage = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attidentity = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attgenerated = (char *) pg_malloc(numatts * sizeof(char));
+		tbinfo->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		tbinfo->attencdet = (bool *) pg_malloc(numatts * sizeof(bool));
+		tbinfo->attencalg = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char));
@@ -8425,6 +8615,22 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->attidentity[j] = *(PQgetvalue(res, r, i_attidentity));
 			tbinfo->attgenerated[j] = *(PQgetvalue(res, r, i_attgenerated));
 			tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
+			if (!PQgetisnull(res, r, i_attcek))
+			{
+				Oid		attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[j] = NULL;
+			if (!PQgetisnull(res, r, i_attencdet))
+				tbinfo->attencdet[j] = (PQgetvalue(res, r, i_attencdet)[0] == 't');
+			else
+				tbinfo->attencdet[j] = 0;
+			if (!PQgetisnull(res, r, i_attencalg))
+				tbinfo->attencalg[j] = atoi(PQgetvalue(res, r, i_attencalg));
+			else
+				tbinfo->attencalg[j] = 0;
 			tbinfo->attisdropped[j] = (PQgetvalue(res, r, i_attisdropped)[0] == 't');
 			tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
 			tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
@@ -9943,6 +10149,12 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_OPFAMILY:
 			dumpOpfamily(fout, (const OpfamilyInfo *) dobj);
 			break;
+		case DO_CEK:
+			dumpColumnEncryptionKey(fout, (const CekInfo *) dobj);
+			break;
+		case DO_CMK:
+			dumpColumnMasterKey(fout, (const CmkInfo *) dobj);
+			break;
 		case DO_COLLATION:
 			dumpCollation(fout, (const CollInfo *) dobj);
 			break;
@@ -13357,6 +13569,141 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * dumpColumnEncryptionKey
+ *	  dump the definition of the given column encryption key
+ */
+static void
+dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcekname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcekname = pg_strdup(fmtId(cekinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN ENCRYPTION KEY %s;\n",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_name(cekinfo->cekcmkalgs[i]));
+		appendPQExpBuffer(query, "encrypted_value = ");
+		appendStringLiteralAH(query, cekinfo->cekencvals[i], fout);
+
+		appendPQExpBuffer(query, ")");
+		if (i < cekinfo->numdata - 1)
+			appendPQExpBuffer(query, ", ");
+	}
+
+	appendPQExpBufferStr(query, ";\n");
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cekinfo->dobj.catId, cekinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cekinfo->dobj.name,
+								  .namespace = cekinfo->dobj.namespace->dobj.name,
+								  .owner = cekinfo->rolname,
+								  .description = "COLUMN ENCRYPTION KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cekinfo->dobj.dumpId, InvalidDumpId, "COLUMN ENCRYPTION KEY",
+				qcekname, NULL, cekinfo->dobj.namespace->dobj.name,
+				cekinfo->rolname, &cekinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcekname);
+}
+
+/*
+ * dumpColumnMasterKey
+ *	  dump the definition of the given column master key
+ */
+static void
+dumpColumnMasterKey(Archive *fout, const CmkInfo *cmkinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcmkname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcmkname = pg_strdup(fmtId(cmkinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN MASTER KEY %s;\n",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "realm = ");
+	appendStringLiteralAH(query, cmkinfo->cmkrealm, fout);
+
+	appendPQExpBufferStr(query, ");\n");
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cmkinfo->dobj.catId, cmkinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cmkinfo->dobj.name,
+								  .namespace = cmkinfo->dobj.namespace->dobj.name,
+								  .owner = cmkinfo->rolname,
+								  .description = "COLUMN MASTER KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN MASTER KEY", qcmkname,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cmkinfo->dobj.dumpId, InvalidDumpId, "COLUMN MASTER KEY",
+				qcmkname, NULL, cmkinfo->dobj.namespace->dobj.name,
+				cmkinfo->rolname, &cmkinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15442,6 +15789,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[j]));
+						/*
+						 * To reduce output size, we don't print the default
+						 * of encryption_type, but we do print the default of
+						 * algorithm, since we might want to change to a new
+						 * default algorithm sometime in the future.
+						 */
+						if (tbinfo->attencdet[j])
+							appendPQExpBuffer(q, "encryption_type = deterministic, ");
+						appendPQExpBuffer(q, "algorithm = '%s')",
+										  get_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -18010,6 +18373,8 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_ACCESS_METHOD:
 			case DO_OPCLASS:
 			case DO_OPFAMILY:
+			case DO_CEK:
+			case DO_CMK:
 			case DO_COLLATION:
 			case DO_CONVERSION:
 			case DO_TABLE:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index cdca0b993d..d4a2e595d0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -47,6 +47,8 @@ typedef enum
 	DO_ACCESS_METHOD,
 	DO_OPCLASS,
 	DO_OPFAMILY,
+	DO_CEK,
+	DO_CMK,
 	DO_COLLATION,
 	DO_CONVERSION,
 	DO_TABLE,
@@ -332,6 +334,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	struct _CekInfo **attcek;
+	int		   *attencalg;
+	bool	   *attencdet;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
 	char	   *attalign;		/* attribute align, used by binary_upgrade */
 	bool	   *attislocal;		/* true if attr has local definition */
@@ -663,6 +668,32 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	struct _CmkInfo **cekcmks;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -683,6 +714,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -710,6 +743,8 @@ extern AccessMethodInfo *getAccessMethods(Archive *fout, int *numAccessMethods);
 extern OpclassInfo *getOpclasses(Archive *fout, int *numOpclasses);
 extern OpfamilyInfo *getOpfamilies(Archive *fout, int *numOpfamilies);
 extern CollInfo *getCollations(Archive *fout, int *numCollations);
+extern void getColumnEncryptionKeys(Archive *fout);
+extern void getColumnMasterKeys(Archive *fout);
 extern ConvInfo *getConversions(Archive *fout, int *numConversions);
 extern TableInfo *getTables(Archive *fout, int *numTables);
 extern void getOwnedSeqs(Archive *fout, TableInfo tblinfo[], int numTables);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 8266c117a3..d3dacd39da 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -69,6 +69,8 @@ enum dbObjectTypePriorities
 	PRIO_TSTEMPLATE,
 	PRIO_TSDICT,
 	PRIO_TSCONFIG,
+	PRIO_CMK,
+	PRIO_CEK,
 	PRIO_FDW,
 	PRIO_FOREIGN_SERVER,
 	PRIO_TABLE,
@@ -111,6 +113,8 @@ static const int dbObjectTypePriority[] =
 	PRIO_ACCESS_METHOD,			/* DO_ACCESS_METHOD */
 	PRIO_OPFAMILY,				/* DO_OPCLASS */
 	PRIO_OPFAMILY,				/* DO_OPFAMILY */
+	PRIO_CEK,					/* DO_CEK */
+	PRIO_CMK,					/* DO_CMK */
 	PRIO_COLLATION,				/* DO_COLLATION */
 	PRIO_CONVERSION,			/* DO_CONVERSION */
 	PRIO_TABLE,					/* DO_TABLE */
@@ -1322,6 +1326,16 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "OPERATOR FAMILY %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_CEK:
+			snprintf(buf, bufsize,
+					 "COLUMN ENCRYPTION KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
+		case DO_CMK:
+			snprintf(buf, bufsize,
+					 "COLUMN MASTER KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_COLLATION:
 			snprintf(buf, bufsize,
 					 "COLLATION %s  (ID %d OID %u)",
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index cd421c5944..6530fb81a2 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -93,6 +93,7 @@ static bool dosync = true;
 
 static int	binary_upgrade = 0;
 static int	column_inserts = 0;
+static int	decrypt_encrypted_columns = 0;
 static int	disable_dollar_quoting = 0;
 static int	disable_triggers = 0;
 static int	if_exists = 0;
@@ -154,6 +155,7 @@ main(int argc, char *argv[])
 		{"attribute-inserts", no_argument, &column_inserts, 1},
 		{"binary-upgrade", no_argument, &binary_upgrade, 1},
 		{"column-inserts", no_argument, &column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &decrypt_encrypted_columns, 1},
 		{"disable-dollar-quoting", no_argument, &disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &disable_triggers, 1},
 		{"exclude-database", required_argument, NULL, 6},
@@ -424,6 +426,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --binary-upgrade");
 	if (column_inserts)
 		appendPQExpBufferStr(pgdumpopts, " --column-inserts");
+	if (decrypt_encrypted_columns)
+		appendPQExpBufferStr(pgdumpopts, " --decrypt-encrypted-columns");
 	if (disable_dollar_quoting)
 		appendPQExpBufferStr(pgdumpopts, " --disable-dollar-quoting");
 	if (disable_triggers)
@@ -649,6 +653,7 @@ help(void)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --exclude-database=PATTERN   exclude databases whose name matches PATTERN\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 187e4b8d07..9a8c032c97 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -719,6 +719,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
 	'ALTER FOREIGN DATA WRAPPER dummy OWNER TO' => {
 		regexp => qr/^ALTER FOREIGN DATA WRAPPER dummy OWNER TO .+;/m,
 		like   => { %full_runs, section_pre_data => 1, },
@@ -1319,6 +1331,26 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1737,6 +1769,26 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3570,6 +3622,26 @@
 		unlike => { no_privs => 1, },
 	},
 
+	'GRANT USAGE ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
+	'GRANT USAGE ON COLUMN MASTER KEY cmk1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
 	'GRANT USAGE ON FOREIGN DATA WRAPPER dummy' => {
 		create_order => 85,
 		create_sql   => 'GRANT USAGE ON FOREIGN DATA WRAPPER dummy
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..0d6a46d24b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 99e28f607e..8f21fabd99 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1539,7 +1539,7 @@ describeOneTableDetails(const char *schemaname,
 	bool		printTableInitialized = false;
 	int			i;
 	char	   *view_def = NULL;
-	char	   *headers[12];
+	char	   *headers[13];
 	PQExpBufferData title;
 	PQExpBufferData tmpbuf;
 	int			cols;
@@ -1555,6 +1555,7 @@ describeOneTableDetails(const char *schemaname,
 				fdwopts_col = -1,
 				attstorage_col = -1,
 				attcompression_col = -1,
+				attcekname_col = -1,
 				attstattarget_col = -1,
 				attdescr_col = -1;
 	int			numrows;
@@ -1577,6 +1578,8 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attusertypid;
+	const char *attusertypmod;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1853,7 +1856,17 @@ describeOneTableDetails(const char *schemaname,
 	cols = 0;
 	printfPQExpBuffer(&buf, "SELECT a.attname");
 	attname_col = cols++;
-	appendPQExpBufferStr(&buf, ",\n  pg_catalog.format_type(a.atttypid, a.atttypmod)");
+	if (pset.sversion >= 160000)
+	{
+		attusertypid = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END";
+		attusertypmod = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END";
+	}
+	else
+	{
+		attusertypid = "a.atttypid";
+		attusertypmod = "a.atttypmod";
+	}
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, %s)", attusertypid, attusertypmod);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1866,7 +1879,8 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		attrdef_col = cols++;
 		attnotnull_col = cols++;
-		appendPQExpBufferStr(&buf, ",\n  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
+		appendPQExpBufferStr(&buf, ",\n"
+							 "  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
 							 "   WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) AS attcollation");
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
@@ -1918,6 +1932,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			appendPQExpBufferStr(&buf, ",\n  (SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek) AS attcekname");
+			attcekname_col = cols++;
+		}
+
 		/* stats target, if relevant to relkind */
 		if (tableinfo.relkind == RELKIND_RELATION ||
 			tableinfo.relkind == RELKIND_INDEX ||
@@ -2041,6 +2067,8 @@ describeOneTableDetails(const char *schemaname,
 		headers[cols++] = gettext_noop("Storage");
 	if (attcompression_col >= 0)
 		headers[cols++] = gettext_noop("Compression");
+	if (attcekname_col >= 0)
+		headers[cols++] = gettext_noop("Encryption");
 	if (attstattarget_col >= 0)
 		headers[cols++] = gettext_noop("Stats target");
 	if (attdescr_col >= 0)
@@ -2133,6 +2161,17 @@ describeOneTableDetails(const char *schemaname,
 							  false, false);
 		}
 
+		/* Column encryption */
+		if (attcekname_col >= 0)
+		{
+			if (!PQgetisnull(res, i, attcekname_col))
+				printTableAddCell(&cont, PQgetvalue(res, i, attcekname_col),
+								  false, false);
+			else
+				printTableAddCell(&cont, "",
+								  false, false);
+		}
+
 		/* Statistics target, if the relkind supports this feature */
 		if (attstattarget_col >= 0)
 			printTableAddCell(&cont, PQgetvalue(res, i, attstattarget_col),
@@ -4486,6 +4525,152 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cekacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cmkacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cmk.oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 554fe86725..1cf8f72176 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1729966959 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,8 @@ helpVariables(unsigned short int pager)
 		  "    true if last query failed, else false\n");
 	HELP0("  FETCH_COUNT\n"
 		  "    the number of result rows to fetch and display at a time (0 = unlimited)\n");
+	HELP0("  HIDE_COLUMN_ENCRYPTION\n"
+		  "    if set, column encryption details are not displayed\n");
 	HELP0("  HIDE_TABLEAM\n"
 		  "    if set, table access methods are not displayed\n");
 	HELP0("  HIDE_TOAST_COMPRESSION\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 73d4b393bc..010bc5a6d5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,7 @@ typedef struct _psqlSettings
 	bool		quiet;
 	bool		singleline;
 	bool		singlestep;
+	bool		hide_column_encryption;
 	bool		hide_compression;
 	bool		hide_tableam;
 	int			fetch_count;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 5a28b6f713..6736505c3a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -1188,6 +1188,13 @@ hide_compression_hook(const char *newval)
 							 &pset.hide_compression);
 }
 
+static bool
+hide_column_encryption_hook(const char *newval)
+{
+	return ParseVariableBool(newval, "HIDE_COLUMN_ENCRYPTION",
+							 &pset.hide_column_encryption);
+}
+
 static bool
 hide_tableam_hook(const char *newval)
 {
@@ -1259,6 +1266,9 @@ EstablishVariableSpace(void)
 	SetVariableHooks(pset.vars, "SHOW_CONTEXT",
 					 show_context_substitute_hook,
 					 show_context_hook);
+	SetVariableHooks(pset.vars, "HIDE_COLUMN_ENCRYPTION",
+					 bool_substitute_hook,
+					 hide_column_encryption_hook);
 	SetVariableHooks(pset.vars, "HIDE_TOAST_COMPRESSION",
 					 bool_substitute_hook,
 					 hide_compression_hook);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 8f12af799b..2f0ca71981 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -933,6 +933,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1223,6 +1237,8 @@ static const pgsql_thing_t words_after_create[] = {
 	{"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", NULL, NULL, &Query_for_list_of_collations},
+	{"COLUMN ENCRYPTION KEY", NULL, NULL, NULL},
+	{"COLUMN MASTER KEY KEY", NULL, NULL, NULL},
 
 	/*
 	 * CREATE CONSTRAINT TRIGGER is not supported here because it is designed
@@ -1705,7 +1721,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1952,6 +1968,22 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "COLLATION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "REFRESH VERSION", "RENAME TO", "SET SCHEMA");
 
+	/* ALTER/DROP COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "ENCRYPTION", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_ceks);
+
+	/* ALTER COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("ADD VALUE (", "DROP VALUE (", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2894,6 +2926,26 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("true", "false");
 	}
 
+	/* CREATE/ALTER/DROP COLUMN ... KEY */
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN"))
+		COMPLETE_WITH("ENCRYPTION", "MASTER");
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN", "ENCRYPTION|MASTER"))
+		COMPLETE_WITH("KEY");
+
+	/* CREATE COLUMN ENCRYPTION KEY */
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("VALUES");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH", "VALUES"))
+		COMPLETE_WITH("(");
+
+	/* CREATE COLUMN MASTER KEY */
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("(");
+
 	/* CREATE DATABASE */
 	else if (Matches("CREATE", "DATABASE", MatchAny))
 		COMPLETE_WITH("OWNER", "TEMPLATE", "ENCODING", "TABLESPACE",
@@ -3605,7 +3657,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 	else if (Matches("DISCARD"))
-		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP");
+		COMPLETE_WITH("ALL", "COLUMN ENCRYPTION KEYS", "PLANS", "SEQUENCES", "TEMP");
 
 /* DO */
 	else if (Matches("DO"))
@@ -3619,6 +3671,7 @@ psql_completion(const char *text, int start, int end)
 			 Matches("DROP", "ACCESS", "METHOD", MatchAny) ||
 			 (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
 			  ends_with(prev_wd, ')')) ||
+			 Matches("DROP", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
 			 Matches("DROP", "EVENT", "TRIGGER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "TABLE", MatchAny) ||
@@ -3931,6 +3984,8 @@ psql_completion(const char *text, int start, int end)
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"COLUMN ENCRYPTION KEY",
+											"COLUMN MASTER KEY",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4046,6 +4101,16 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+	/* Complete "GRANT/REVOKE * ON COLUMN ENCRYPTION|MASTER KEY *" with TO/FROM */
+	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
+			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny))
+	{
+		if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny, MatchAny))
+			COMPLETE_WITH("TO");
+		else
+			COMPLETE_WITH("FROM");
+	}
+
 	/* Complete "GRANT/REVOKE * ON FOREIGN DATA WRAPPER *" with TO/FROM */
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny))
diff --git a/src/common/Makefile b/src/common/Makefile
index 113029bf7b..73dce1150e 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			return "AEAD_AES_128_CBC_HMAC_SHA_256";
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			return "AEAD_AES_192_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			return "AEAD_AES_256_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			return "AEAD_AES_256_CBC_HMAC_SHA_512";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index 41bd58ebdf..3695d3285b 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -4,6 +4,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 747ecb800d..4e384bbcdb 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,9 +20,13 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
+extern void DiscardColumnEncryptionKeys(void);
+
 extern void debugStartup(DestReceiver *self, int operation,
 						 TupleDesc typeinfo);
 extern bool debugtup(TupleTableSlot *slot, DestReceiver *self);
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index ffd5e9dc82..eb59e73c0a 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -92,6 +92,9 @@ typedef enum ObjectClass
 	OCLASS_TYPE,				/* pg_type */
 	OCLASS_CAST,				/* pg_cast */
 	OCLASS_COLLATION,			/* pg_collation */
+	OCLASS_CEK,					/* pg_colenckey */
+	OCLASS_CEKDATA,				/* pg_colenckeydata */
+	OCLASS_CMK,					/* pg_colmasterkey */
 	OCLASS_CONSTRAINT,			/* pg_constraint */
 	OCLASS_CONVERSION,			/* pg_conversion */
 	OCLASS_DEFAULT,				/* pg_attrdef */
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index d01ab504b6..758696b539 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 3179be09d3..9e2c5256f7 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,9 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_colmasterkey.h',
+  'pg_colenckey.h',
+  'pg_colenckeydata.h',
 ]
 
 bki_data = [
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index f64a0ec26b..d0b9e8458d 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -115,6 +115,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c4d6adcd3e..c58b79e3a7 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 5b950129de..0e9e85ebf3 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index b561e17781..7910175a6a 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,17 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	bool		attislocal BKI_DEFAULT(t);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey);
+
+	/*
+	 * User-visible type and typmod, currently used for encrypted columns.
+	 * These are only set to nondefault values if they are different from
+	 * atttypid and attypmod.
+	 */
+	Oid			attusertypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+	int32		attusertypmod BKI_DEFAULT(-1);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..c57fa18a27
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+/* ----------------
+ *		pg_colenckey definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckey
+ * ----------------
+ */
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+	Oid			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		cekacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_TOAST(pg_colenckey, 8263, 8264);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_nsp_index, 8242, ColumnEncKeyNameNspIndexId, on pg_colenckey using btree(cekname name_ops, ceknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkeydata.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+/* ----------------
+ *		pg_colenckeydata definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckeydata
+ * ----------------
+ */
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..d3bfd36279
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+	aclitem		cmkacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, on pg_colmasterkey using btree(cmkname name_ops, cmknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c867d99563..ff06d52fd0 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index b2cdea66c4..114279fa64 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 91587b99d0..c21052a3f7 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 505595620e..b410388a42 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6355,6 +6355,14 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -7142,6 +7150,68 @@
   proname => 'fmgr_sql_validator', provolatile => 's', prorettype => 'void',
   proargtypes => 'oid', prosrc => 'fmgr_sql_validator' },
 
+{ oid => '8265',
+  descr => 'user privilege on column encryption key by username, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_encryption_key_privilege_name_name' },
+{ oid => '8266',
+  descr => 'user privilege on column encryption key by username, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_encryption_key_privilege_name_id' },
+{ oid => '8267',
+  descr => 'user privilege on column encryption key by user oid, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_encryption_key_privilege_id_name' },
+{ oid => '8268',
+  descr => 'user privilege on column encryption key by user oid, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_encryption_key_privilege_id_id' },
+{ oid => '8269',
+  descr => 'current user privilege on column encryption key by column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_encryption_key_privilege_name' },
+{ oid => '8270',
+  descr => 'current user privilege on column encryption key by column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_encryption_key_privilege_id' },
+
+{ oid => '8271',
+  descr => 'user privilege on column master key by username, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_master_key_privilege_name_name' },
+{ oid => '8272',
+  descr => 'user privilege on column master key by username, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_master_key_privilege_name_id' },
+{ oid => '8273',
+  descr => 'user privilege on column master key by user oid, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_master_key_privilege_id_name' },
+{ oid => '8274',
+  descr => 'user privilege on column master key by user oid, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_master_key_privilege_id_id' },
+{ oid => '8275',
+  descr => 'current user privilege on column master key by column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_master_key_privilege_name' },
+{ oid => '8276',
+  descr => 'current user privilege on column master key by column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_master_key_privilege_id' },
+
 { oid => '2250',
   descr => 'user privilege on database by username, database name',
   proname => 'has_database_privilege', provolatile => 's', prorettype => 'bool',
@@ -11939,4 +12009,37 @@
   proname => 'any_value_transfn', prorettype => 'anyelement',
   proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8245',
+  proname => 'pg_encrypted_det_eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'pg_encrypted_det_ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 92bcaf2c73..b9d3920a97 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,17 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+# Note: typstorage 'e' since compression is not useful for encrypted data
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 519e570c8c..3c7ab2a8fe 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -294,6 +294,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPCATEGORY_USER		'U'
 #define  TYPCATEGORY_BITSTRING	'V' /* er ... "varbit"? */
 #define  TYPCATEGORY_UNKNOWN	'X'
+#define  TYPCATEGORY_ENCRYPTED	'Y'
 #define  TYPCATEGORY_INTERNAL	'Z'
 
 #define  TYPALIGN_CHAR			'c' /* char alignment (i.e. unaligned) */
diff --git a/src/include/commands/colenccmds.h b/src/include/commands/colenccmds.h
new file mode 100644
index 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/colenccmds.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COLENCCMDS_H
+#define COLENCCMDS_H
+
+#include "catalog/objectaddress.h"
+#include "parser/parse_node.h"
+
+extern ObjectAddress CreateCEK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt);
+extern ObjectAddress CreateCMK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e7c2b91a58..11f88c59ce 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,4 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml>.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index ac6407e9f6..39a286e58a 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -164,6 +164,7 @@ typedef struct Port
 	 */
 	char	   *database_name;
 	char	   *user_name;
+	bool		column_encryption_enabled;
 	char	   *cmdline_options;
 	List	   *guc_options;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 371aa0ffc5..4253d531f8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -721,6 +721,7 @@ typedef struct ColumnDef
 	char	   *colname;		/* name of column */
 	TypeName   *typeName;		/* type of column */
 	char	   *compression;	/* compression method for column */
+	List	   *encryption;		/* encryption info for column */
 	int			inhcount;		/* number of times column is inherited */
 	bool		is_local;		/* column has local (non-inherited) def'n */
 	bool		is_not_null;	/* NOT NULL constraint specified? */
@@ -757,11 +758,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1979,6 +1981,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2166,6 +2171,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
@@ -3644,6 +3674,7 @@ typedef struct CheckPointStmt
 typedef enum DiscardMode
 {
 	DISCARD_ALL,
+	DISCARD_COLUMN_ENCRYPTION_KEYS,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
 	DISCARD_TEMP
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..c6da932042 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -149,6 +149,7 @@ PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("encryption", ENCRYPTION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -228,6 +229,7 @@ PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL)
 PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("language", LANGUAGE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("large", LARGE_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -250,6 +252,7 @@ PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("master", MASTER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index d4865e50f6..26e1cd617a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..a5e61c9f5b 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -29,6 +29,8 @@ PG_CMDTAG(CMDTAG_ALTER_ACCESS_METHOD, "ALTER ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_AGGREGATE, "ALTER AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CAST, "ALTER CAST", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_COLLATION, "ALTER COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY, "ALTER COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_MASTER_KEY, "ALTER COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONSTRAINT, "ALTER CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONVERSION, "ALTER CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_DATABASE, "ALTER DATABASE", false, false, false)
@@ -86,6 +88,8 @@ PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, fals
 PG_CMDTAG(CMDTAG_CREATE_AGGREGATE, "CREATE AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CAST, "CREATE CAST", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_COLLATION, "CREATE COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY, "CREATE COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_MASTER_KEY, "CREATE COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONSTRAINT, "CREATE CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONVERSION, "CREATE CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_DATABASE, "CREATE DATABASE", false, false, false)
@@ -130,6 +134,7 @@ PG_CMDTAG(CMDTAG_DECLARE_CURSOR, "DECLARE CURSOR", false, false, false)
 PG_CMDTAG(CMDTAG_DELETE, "DELETE", false, false, true)
 PG_CMDTAG(CMDTAG_DISCARD, "DISCARD", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
+PG_CMDTAG(CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS, "DISCARD COLUMN ENCRYPTION KEYS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false)
@@ -138,6 +143,8 @@ PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CAST, "DROP CAST", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_COLLATION, "DROP COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_ENCRYPTION_KEY, "DROP COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_MASTER_KEY, "DROP COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONSTRAINT, "DROP CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONVERSION, "DROP CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_DATABASE, "DROP DATABASE", false, false, false)
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2..0c73022833 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -159,6 +159,8 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
+#define ACL_ALL_RIGHTS_CEK			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_CMK			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..1dac24e4b4 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -163,6 +163,7 @@ extern bool type_is_rowtype(Oid typid);
 extern bool type_is_enum(Oid typid);
 extern bool type_is_range(Oid typid);
 extern bool type_is_multirange(Oid typid);
+extern bool type_is_encrypted(Oid typid);
 extern void get_type_category_preferred(Oid typid,
 										char *typcategory,
 										bool *typispreferred);
@@ -202,6 +203,9 @@ extern Oid	get_publication_oid(const char *pubname, bool missing_ok);
 extern char *get_publication_name(Oid pubid, bool missing_ok);
 extern Oid	get_subscription_oid(const char *subname, bool missing_ok);
 extern char *get_subscription_name(Oid subid, bool missing_ok);
+extern char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok);
+extern char *get_cmk_name(Oid cmkid, bool missing_ok);
 
 #define type_is_array(typid)  (get_element_type(typid) != InvalidOid)
 /* type_is_array_domain accepts both plain arrays and domains over arrays */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index a443181d41..8ecacb29df 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d5d50ceab4..bf1d527c1a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAMENSP,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAMENSP,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c18e914228..10dfda8016 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,7 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 97e47f0585..24e326b448 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -341,6 +341,14 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	{"cmklookup", "PGCMKLOOKUP", "", NULL,
+		"CMK-Lookup", "", 64,
+	offsetof(struct pg_conn, cmklookup)},
+
+	{"column_encryption", "PGCOLUMNENCRYPTION", "0", NULL,
+		"Column-Encryption", "", 1,
+	offsetof(struct pg_conn, column_encryption_setting)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -1412,6 +1420,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4056,6 +4086,22 @@ freePGconn(PGconn *conn)
 	free(conn->krbsrvname);
 	free(conn->gsslib);
 	free(conn->connip);
+	free(conn->cmklookup);
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		free(conn->cmks[i].cmkname);
+		free(conn->cmks[i].cmkrealm);
+	}
+	free(conn->cmks);
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		if (conn->ceks[i].cekdata)
+		{
+			explicit_bzero(conn->ceks[i].cekdata, conn->ceks[i].cekdatalen);
+			free(conn->ceks[i].cekdata);
+		}
+	}
+	free(conn->ceks);
 	/* Note that conn->Pfdebug is not ours to close or free */
 	free(conn->write_err_msg);
 	free(conn->inBuffer);
diff --git a/src/interfaces/libpq/fe-encrypt-openssl.c b/src/interfaces/libpq/fe-encrypt-openssl.c
new file mode 100644
index 0000000000..9baa47da13
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,839 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt-openssl.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include "fe-encrypt.h"
+#include "libpq-int.h"
+
+#include "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+#define pqResultAlloc(res, nBytes, isBinary) malloc(nBytes)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = NULL;
+	EVP_PKEY_CTX *ctx = NULL;
+	unsigned char *out = NULL;
+	size_t		outlen;
+
+	switch (cmkalg)
+	{
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			md = EVP_sha1();
+			break;
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0 ||
+		EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		free(out);
+		out = NULL;
+		goto fail;
+	}
+
+	*tolen = outlen;
+
+fail:
+	EVP_PKEY_CTX_free(ctx);
+	EVP_PKEY_free(key);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05>
+ * for encrypting and decrypting column values.
+ */
+
+#ifdef TEST_ENCRYPT
+
+/*
+ * Test data from
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5>
+ */
+
+/*
+ * The different test cases just use different prefixes of K, so one constant
+ * is enough here.
+ */
+static const unsigned char K[] = {
+	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+	0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+	0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+	0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+};
+
+static const unsigned char P[] = {
+	0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20,
+	0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75,
+	0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65,
+	0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62,
+	0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69,
+	0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66,
+	0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f,
+	0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65,
+};
+
+static const unsigned char test_IV[] = {
+	0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04,
+};
+
+static const unsigned char test_A[] = {
+	0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63,
+	0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20,
+	0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+static const EVP_CIPHER *
+pg_cekalg_to_openssl_cipher(int cekalg)
+{
+	const EVP_CIPHER *cipher;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			cipher = EVP_aes_128_cbc();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			cipher = EVP_aes_192_cbc();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			cipher = EVP_aes_256_cbc();
+			break;
+		default:
+			cipher = NULL;
+	}
+
+	return cipher;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+static const EVP_MD *
+pg_cekalg_to_openssl_md(int cekalg)
+{
+	const EVP_MD *md;
+
+	switch (cekalg)
+	{
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			md = EVP_sha256();
+			break;
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			md = EVP_sha384();
+			break;
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			md = EVP_sha512();
+			break;
+		default:
+			md = NULL;
+	}
+
+	return md;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+static int
+md_key_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 16;
+	else if (md == EVP_sha384())
+		return 24;
+	else if (md == EVP_sha512())
+		return 32;
+	else
+		return -1;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+static int
+md_hash_length(const EVP_MD *md)
+{
+	if (md == EVP_sha256())
+		return 32;
+	else if (md == EVP_sha384())
+		return 48;
+	else if (md == EVP_sha512())
+		return 64;
+	else
+		return -1;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+static bool
+get_message_auth_tag(const EVP_MD *md,
+					 const unsigned char *mac_key, int mac_key_len,
+					 const unsigned char *encr, int encrlen,
+					 unsigned char *md_value, size_t *md_len_p,
+					 const char **errmsgp)
+{
+	static char msgbuf[1024];
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	int64		al;
+	bool		result = false;
+
+	if (encrlen < 0)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("encrypted value has invalid length"));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, mac_key, mac_key_len);
+	if (!pkey)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("could not allocate key for HMAC: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, buf, bufsize))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, md_len_p))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	Assert(*md_len_p == md_hash_length(md));
+
+	/* truncate output to half the length, per spec */
+	*md_len_p /= 2;
+
+	result = true;
+fail:
+	free(buf);
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+
+/*
+ * Decrypt a column value
+ */
+unsigned char *
+decrypt_value(PGresult *res, const PGCEK *cek, int cekalg, const unsigned char *input, int inputlen, const char **errmsgp)
+{
+	static char msgbuf[1024];
+
+	const unsigned char *iv = NULL;
+	size_t		ivlen;
+
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *decr;
+	int			decrlen,
+				decrlen2;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized encryption algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized digest algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("column encryption key has wrong length for algorithm (has: %zu, required: %d)"),
+				 cek->cekdatalen, key_len);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_memcmp(input + (inputlen - md_len), md_value, md_len) != 0)
+	{
+		*errmsgp = libpq_gettext("MAC mismatch");
+		goto fail;
+	}
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	iv = input;
+	input += ivlen;
+	inputlen -= ivlen;
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = inputlen + EVP_CIPHER_CTX_block_size(evp_cipher_ctx) + 1;
+	buf = pqResultAlloc(res, bufsize, false);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	decr = buf;
+	if (!EVP_DecryptUpdate(evp_cipher_ctx, decr, &decrlen, input, inputlen - md_len))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	if (!EVP_DecryptFinal_ex(evp_cipher_ctx, decr + decrlen, &decrlen2))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	decrlen += decrlen2;
+	Assert(decrlen < bufsize);
+	decr[decrlen] = '\0';
+	result = decr;
+
+fail:
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	return result;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#ifndef TEST_ENCRYPT
+static bool
+make_siv(PGconn *conn,
+		 unsigned char *iv, size_t ivlen,
+		 const EVP_MD *md,
+		 const unsigned char *iv_key, int iv_key_len,
+		 const unsigned char *plaintext, int plaintext_len)
+{
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	bool		result = false;
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, iv_key, iv_key_len);
+	if (!pkey)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	Assert(md_len == md_hash_length(md));
+	memcpy(iv, md_value, ivlen);
+
+	result = true;
+fail:
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+#endif							/* TEST_ENCRYPT */
+
+/*
+ * Encrypt a column value
+ */
+unsigned char *
+encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg, const unsigned char *value, int *nbytesp, bool enc_det)
+{
+	int			nbytes = *nbytesp;
+	unsigned char iv[EVP_MAX_IV_LENGTH];
+	size_t		ivlen;
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_key;
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *encr;
+	int			encrlen,
+				encrlen2;
+
+	const char *errmsg;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		buf2size;
+	unsigned char *buf2 = NULL;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	Assert(ivlen <= sizeof(iv));
+	if (enc_det)
+	{
+#ifndef TEST_ENCRYPT
+		make_siv(conn, iv, ivlen, md, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	bufsize = ivlen + (nbytes + 2 * EVP_CIPHER_CTX_block_size(evp_cipher_ctx) - 1);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	encrlen += encrlen2;
+
+	encr -= ivlen;
+	encrlen += ivlen;
+
+	Assert(encrlen <= bufsize);
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  encr, encrlen,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf2, encr, encrlen);
+	memcpy(buf2 + encrlen, md_value, md_len);
+
+	result = buf2;
+	nbytes = buf2size;
+
+fail:
+	free(buf);
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	*nbytesp = nbytes;
+	return result;
+}
+
+
+/*
+ * Run test cases
+ */
+#ifdef TEST_ENCRYPT
+
+static void
+debug_print_hex(const char *name, const unsigned char *val, int len)
+{
+	printf("%s =", name);
+	for (int i = 0; i < len; i++)
+	{
+		if (i % 16 == 0)
+			printf("\n");
+		else
+			printf(" ");
+		printf("%02x", val[i]);
+	}
+	printf("\n");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, K_len);
+
+	C = encrypt_value(NULL, &cek, alg, P, &nbytes, true);
+	debug_print_hex("C", C, nbytes);
+}
+
+int
+main(int argc, char **argv)
+{
+	printf("5.1\n");
+	test_case(PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256, K, 32, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FE_ENCRYPT_H
+#define FE_ENCRYPT_H
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+extern unsigned char *decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+											int fromlen, const unsigned char *from,
+											int *tolen);
+
+extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg,
+									const unsigned char *input, int inputlen,
+									const char **errmsgp);
+
+extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg,
+									const unsigned char *value, int *nbytesp, bool enc_det);
+
+#endif							/* FE_ENCRYPT_H */
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index ec62550e38..4bd9e4e309 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,420 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+int
+pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+					  const char *keyrealm)
+{
+	char	   *keyname_copy;
+	char	   *keyrealm_copy;
+	bool		found;
+
+	keyname_copy = strdup(keyname);
+	if (!keyname_copy)
+		return EOF;
+	keyrealm_copy = strdup(keyrealm);
+	if (!keyrealm_copy)
+	{
+		free(keyname_copy);
+		return EOF;
+	}
+
+	found = false;
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		struct pg_cmk *checkcmk = &conn->cmks[i];
+
+		/* replace existing? */
+		if (checkcmk->cmkid == keyid)
+		{
+			free(checkcmk->cmkname);
+			free(checkcmk->cmkrealm);
+			checkcmk->cmkname = keyname_copy;
+			checkcmk->cmkrealm = keyrealm_copy;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newncmks;
+		struct pg_cmk *newcmks;
+		struct pg_cmk *newcmk;
+
+		newncmks = conn->ncmks + 1;
+		if (newncmks <= 0)
+			return EOF;
+		newcmks = realloc(conn->cmks, newncmks * sizeof(struct pg_cmk));
+		if (!newcmks)
+		{
+			free(keyname_copy);
+			free(keyrealm_copy);
+			return EOF;
+		}
+
+		newcmk = &newcmks[newncmks - 1];
+		newcmk->cmkid = keyid;
+		newcmk->cmkname = keyname_copy;
+		newcmk->cmkrealm = keyrealm_copy;
+
+		conn->ncmks = newncmks;
+		conn->cmks = newcmks;
+	}
+
+	return 0;
+}
+
+/*
+ * Replace placeholders in input string.  Return value malloc'ed.
+ */
+static char *
+replace_cmk_placeholders(const char *in, const char *cmkname, const char *cmkrealm, int cmkalg, const char *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					p++;
+					break;
+				case 'r':
+					appendPQExpBufferStr(&buf, cmkrealm);
+					p++;
+					break;
+				default:
+					appendPQExpBufferChar(&buf, p[0]);
+			}
+		}
+		else
+			appendPQExpBufferChar(&buf, p[0]);
+	}
+
+	return buf.data;
+}
+
+#ifndef USE_SSL
+/*
+ * Dummy implementation for non-SSL builds
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	return NULL;
+}
+#endif
+
+/*
+ * Decrypt a CEK using the given CMK.  The ciphertext is passed in
+ * "from" and "fromlen".  Return the decrypted value in a malloc'ed area, its
+ * length via "tolen".  Return NULL on error; add error messages directly to
+ * "conn".
+ */
+static unsigned char *
+decrypt_cek(PGconn *conn, const PGCMK *cmk, int cmkalg,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	char	   *cmklookup;
+	bool		found = false;
+	unsigned char *result = NULL;
+
+	if (!conn->cmklookup || !conn->cmklookup[0])
+	{
+		libpq_append_conn_error(conn, "column master key lookup is not configured");
+		return NULL;
+	}
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		return NULL;
+	}
+
+	/*
+	 * Analyze semicolon-separated list
+	 */
+	for (char *s = strtok(cmklookup, ";"); s; s = strtok(NULL, ";"))
+	{
+		char	   *sep;
+
+		/* split found token at '=' */
+		sep = strchr(s, '=');
+		if (!sep)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', s);
+			break;
+		}
+
+		/* matching realm? */
+		if (strncmp(s, "*", sep - s) == 0 || strncmp(s, cmk->cmkrealm, sep - s) == 0)
+		{
+			char	   *sep2;
+
+			found = true;
+
+			sep2 = strchr(sep, ':');
+			if (!sep2)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, PG_BINARY_R);
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+int
+pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, const unsigned char *value, int len)
+{
+	PGCMK	   *cmk = NULL;
+	unsigned char *plainval = NULL;
+	int			plainvallen = 0;
+	bool		found;
+
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		if (conn->cmks[i].cmkid == cmkid)
+		{
+			cmk = &conn->cmks[i];
+			break;
+		}
+	}
+
+	if (!cmk)
+		return EOF;
+
+	plainval = decrypt_cek(conn, cmk, cmkalg, len, value, &plainvallen);
+	if (!plainval)
+		return EOF;
+
+	found = false;
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		struct pg_cek *checkcek = &conn->ceks[i];
+
+		/* replace existing? */
+		if (checkcek->cekid == keyid)
+		{
+			free(checkcek->cekdata);
+			checkcek->cekdata = plainval;
+			checkcek->cekdatalen = plainvallen;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newnceks;
+		struct pg_cek *newceks;
+		struct pg_cek *newcek;
+
+		newnceks = conn->nceks + 1;
+		if (newnceks <= 0)
+		{
+			free(plainval);
+			return EOF;
+		}
+		newceks = realloc(conn->ceks, newnceks * sizeof(struct pg_cek));
+		if (!newceks)
+		{
+			free(plainval);
+			return EOF;
+		}
+
+		newcek = &newceks[newnceks - 1];
+		newcek->cekid = keyid;
+		newcek->cekdata = plainval;
+		newcek->cekdatalen = plainvallen;
+
+		conn->nceks = newnceks;
+		conn->ceks = newceks;
+	}
+
+	return 0;
+}
 
 /*
  * pqRowProcessor
@@ -1251,13 +1668,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == res->attDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					*errmsgp = libpq_gettext("protocol error: column encryption key associated with encrypted column was not sent by the server");
+					goto fail;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				if (val == NULL)
+					goto fail;
+#else
+				*errmsgp = libpq_gettext("column encryption not supported by this build");
 				goto fail;
+#endif
+			}
+			else
+			{
+				val = (char *) pqResultAlloc(res, clen + 1, isbinary);
+				if (val == NULL)
+					goto fail;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* copy and zero-terminate the data (even if it's binary) */
+				memcpy(val, columns[i].value, clen);
+				val[clen] = '\0';
+			}
 
 			tup[i].len = clen;
 			tup[i].value = val;
@@ -1500,6 +1955,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1973,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2012,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2128,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2171,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2270,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2319,47 @@ PQsendQueryGuts(PGconn *conn,
 		goto sendFailed;
 
 	/* Send parameter formats */
-	if (nParams > 0 && paramFormats)
+	if (nParams > 0 && (paramFormats || (paramDesc && paramDesc->paramDescs)))
 	{
 		if (pqPutInt(nParams, 2, conn) < 0)
 			goto sendFailed;
+
 		for (i = 0; i < nParams; i++)
 		{
-			if (pqPutInt(paramFormats[i], 2, conn) < 0)
+			int			format = paramFormats ? paramFormats[i] : 0;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2378,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2396,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				bool		enc_det = (paramDesc->paramDescs[i].flags & 0x01) != 0;
+				PGCEK	   *cek = NULL;
+				char	   *enc_paramValue;
+				int			enc_nbytes = nbytes;
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == paramDesc->paramDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					libpq_append_conn_error(conn, "protocol error: column encryption key associated with encrypted parameter was not sent by the server");
+					goto sendFailed;
+				}
+
+				enc_paramValue = (char *) encrypt_value(conn, cek, paramDesc->paramDescs[i].cekalg,
+														(const unsigned char *) paramValue, &enc_nbytes, enc_det);
+				if (!enc_paramValue)
+					goto sendFailed;
+
+				if (pqPutInt(enc_nbytes, 4, conn) < 0 ||
+					pqPutnchar(enc_paramValue, enc_nbytes, conn) < 0)
+					goto sendFailed;
+
+				free(enc_paramValue);
+#else
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2878,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4146,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4194,17 @@ PQfmod(const PGresult *res, int field_num)
 		return 0;
 }
 
+int
+PQfisencrypted(const PGresult *res, int field_num)
+{
+	if (!check_field_number(res, field_num))
+		return false;
+	if (res->attDescs)
+		return (res->attDescs[field_num].cekid != 0);
+	else
+		return false;
+}
+
 char *
 PQcmdStatus(PGresult *res)
 {
@@ -3762,6 +4390,17 @@ PQparamtype(const PGresult *res, int param_num)
 		return InvalidOid;
 }
 
+int
+PQparamisencrypted(const PGresult *res, int param_num)
+{
+	if (!check_param_number(res, param_num))
+		return false;
+	if (res->paramDescs)
+		return (res->paramDescs[param_num].cekid != 0);
+	else
+		return false;
+}
+
 
 /* PQsetnonblocking:
  *	sets the PGconn's database connection non-blocking if the arg is true
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 8ab6a88416..617e641cf6 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -43,6 +43,8 @@ static int	getRowDescriptions(PGconn *conn, int msgLength);
 static int	getParamDescriptions(PGconn *conn, int msgLength);
 static int	getAnotherTuple(PGconn *conn, int msgLength);
 static int	getParameterStatus(PGconn *conn);
+static int	getColumnMasterKey(PGconn *conn);
+static int	getColumnEncryptionKey(PGconn *conn);
 static int	getNotify(PGconn *conn);
 static int	getCopyStart(PGconn *conn, ExecStatusType copytype);
 static int	getReadyForQuery(PGconn *conn);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -358,8 +366,21 @@ pqParseInput3(PGconn *conn)
 					}
 					break;
 				case 't':		/* Parameter Description */
-					if (getParamDescriptions(conn, msgLength))
-						return;
+					if (conn->error_result ||
+						(conn->result != NULL &&
+						 conn->result->resultStatus == PGRES_FATAL_ERROR))
+					{
+						/*
+						 * We've already choked for some reason.  Just discard
+						 * the data till we get to the end of the query.
+						 */
+						conn->inCursor += msgLength;
+					}
+					else
+					{
+						if (getParamDescriptions(conn, msgLength))
+							return;
+					}
 					break;
 				case 'D':		/* Data Row */
 					if (conn->result != NULL &&
@@ -547,6 +568,9 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		int			typlen;
 		int			atttypmod;
 		int			format;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGets(&conn->workBuffer, conn) ||
 			pqGetInt(&tableid, 4, conn) ||
@@ -561,6 +585,23 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, conn) ||
+				pqGetInt(&flags, 2, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -582,6 +623,8 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		result->attDescs[i].typid = typid;
 		result->attDescs[i].typlen = typlen;
 		result->attDescs[i].atttypmod = atttypmod;
+		result->attDescs[i].cekid = cekid;
+		result->attDescs[i].cekalg = cekalg;
 
 		if (format != 1)
 			result->binary = 0;
@@ -685,10 +728,31 @@ getParamDescriptions(PGconn *conn, int msgLength)
 	for (i = 0; i < nparams; i++)
 	{
 		int			typid;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGetInt(&typid, 4, conn))
 			goto not_enough_data;
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&cekalg, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&flags, 2, conn))
+				goto not_enough_data;
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
 		result->paramDescs[i].typid = typid;
+		result->paramDescs[i].cekid = cekid;
+		result->paramDescs[i].cekalg = cekalg;
+		result->paramDescs[i].flags = flags;
 	}
 
 	/* Success! */
@@ -1468,6 +1532,92 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+/*
+ * Attempt to read a ColumnMasterKey message.
+ * Entry: 'y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	int			keyid;
+	char	   *keyname;
+	char	   *keyrealm;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	if (!keyname)
+		return EOF;
+	/* Get the key realm */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyrealm = strdup(conn->workBuffer.data);
+	if (!keyrealm)
+		return EOF;
+	/* And save it */
+	ret = pqSaveColumnMasterKey(conn, keyid, keyname, keyrealm);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(keyname);
+	free(keyrealm);
+
+	return ret;
+}
+
+/*
+ * Attempt to read a ColumnEncryptionKey message.
+ * Entry: 'Y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	int			keyid;
+	int			cmkid;
+	int			cmkalg;
+	char	   *buf;
+	int			vallen;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK ID */
+	if (pqGetInt(&cmkid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK algorithm */
+	if (pqGetInt(&cmkalg, 4, conn) != 0)
+		return EOF;
+	/* Get the key data len */
+	if (pqGetInt(&vallen, 4, conn) != 0)
+		return EOF;
+	/* Get the key data */
+	buf = malloc(vallen);
+	if (!buf)
+		return EOF;
+	if (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2436,9 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	if (conn->column_encryption_enabled)
+		ADD_STARTUP_OPTION("_pq_.column_encryption", "1");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index abaab6a073..5c2793a5eb 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -450,7 +450,8 @@ pqTraceOutputS(FILE *f, const char *message, int *cursor)
 
 /* ParameterDescription */
 static void
-pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -458,12 +459,21 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 	nfields = pqTraceOutputInt16(f, message, cursor);
 
 	for (int i = 0; i < nfields; i++)
+	{
 		pqTraceOutputInt32(f, message, cursor, regress);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+			pqTraceOutputInt16(f, message, cursor);
+		}
+	}
 }
 
 /* RowDescription */
 static void
-pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -479,6 +489,12 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
 		pqTraceOutputInt16(f, message, cursor);
 		pqTraceOutputInt32(f, message, cursor, false);
 		pqTraceOutputInt16(f, message, cursor);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt32(f, message, cursor, false);
+			pqTraceOutputInt16(f, message, cursor);
+		}
 	}
 }
 
@@ -514,6 +530,30 @@ pqTraceOutputW(FILE *f, const char *message, int *cursor, int length)
 		pqTraceOutputInt16(f, message, cursor);
 }
 
+/* ColumnMasterKey */
+static void
+pqTraceOutputy(FILE *f, const char *message, int *cursor, bool regress)
+{
+	fprintf(f, "ColumnMasterKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+}
+
+/* ColumnEncryptionKey */
+static void
+pqTraceOutputY(FILE *f, const char *message, int *cursor, bool regress)
+{
+	int			len;
+
+	fprintf(f, "ColumnEncryptionKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, false);
+	len = pqTraceOutputInt32(f, message, cursor, false);
+	pqTraceOutputNchar(f, len, message, cursor);
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -647,10 +687,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 				fprintf(conn->Pfdebug, "Sync"); /* no message content */
 			break;
 		case 't':				/* Parameter Description */
-			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'T':				/* Row Description */
-			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'v':				/* Negotiate Protocol Version */
 			pqTraceOutputv(conn->Pfdebug, message, &logCursor);
@@ -665,6 +707,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			fprintf(conn->Pfdebug, "Terminate");
 			/* No message content */
 			break;
+		case 'y':
+			pqTraceOutputy(conn->Pfdebug, message, &logCursor, regress);
+			break;
+		case 'Y':
+			pqTraceOutputY(conn->Pfdebug, message, &logCursor, regress);
+			break;
 		case 'Z':				/* Ready For Query */
 			pqTraceOutputZ(conn->Pfdebug, message, &logCursor);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index f3d9220496..cf339a3e53 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -267,6 +267,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -438,6 +440,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,7 @@ extern int	PQfformat(const PGresult *res, int field_num);
 extern Oid	PQftype(const PGresult *res, int field_num);
 extern int	PQfsize(const PGresult *res, int field_num);
 extern int	PQfmod(const PGresult *res, int field_num);
+extern int	PQfisencrypted(const PGresult *res, int field_num);
 extern char *PQcmdStatus(PGresult *res);
 extern char *PQoidStatus(const PGresult *res);	/* old and ugly */
 extern Oid	PQoidValue(const PGresult *res);	/* new and improved */
@@ -540,6 +559,7 @@ extern int	PQgetlength(const PGresult *res, int tup_num, int field_num);
 extern int	PQgetisnull(const PGresult *res, int tup_num, int field_num);
 extern int	PQnparams(const PGresult *res);
 extern Oid	PQparamtype(const PGresult *res, int param_num);
+extern int	PQparamisencrypted(const PGresult *res, int param_num);
 
 /* Describe prepared statements and portals */
 extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index d7ec5ed429..05b5891ffe 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -112,6 +112,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -343,6 +346,26 @@ typedef struct pg_conn_host
 								 * found in password file. */
 } pg_conn_host;
 
+/*
+ * Column encryption support data
+ */
+
+/* column master key */
+typedef struct pg_cmk
+{
+	Oid			cmkid;
+	char	   *cmkname;
+	char	   *cmkrealm;
+} PGCMK;
+
+/* column encryption key */
+typedef struct pg_cek
+{
+	Oid			cekid;
+	unsigned char *cekdata;		/* (decrypted) */
+	size_t		cekdatalen;
+} PGCEK;
+
 /*
  * PGconn stores all the state data associated with a single connection
  * to a backend.
@@ -396,6 +419,8 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *cmklookup;		/* CMK lookup specification */
+	char	   *column_encryption_setting;	/* column_encryption connection parameter */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -477,6 +502,13 @@ struct pg_conn
 	PGVerbosity verbosity;		/* error/notice message verbosity */
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
+	bool		column_encryption_enabled;	/* parsed version of column_encryption_setting */
+
+	/* Column encryption support data */
+	int			ncmks;
+	PGCMK	   *cmks;
+	int			nceks;
+	PGCEK	   *ceks;
 
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
@@ -673,6 +705,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern int	pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+								  const char *keyrealm);
+extern int	pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg,
+									  const unsigned char *value, int len);
 extern int	pqRowProcessor(PGconn *conn, const char **errmsgp);
 extern void pqCommandQueueAdvance(PGconn *conn);
 extern int	PQsendQueryContinue(PGconn *conn, const char *query);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 573fd9b6ea..a6e6b2ada9 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,7 @@ endif
 
 if ssl.found()
   libpq_sources += files('fe-secure-common.c')
+  libpq_sources += files('fe-encrypt-openssl.c')
   libpq_sources += files('fe-secure-openssl.c')
 endif
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': get_option('ssl')},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index b6ed455183..9aef1c7a29 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -3,6 +3,7 @@ CATALOG_NAME     = libpq
 GETTEXT_FILES    = fe-auth.c \
                    fe-auth-scram.c \
                    fe-connect.c \
+                   fe-encrypt-openssl.c \
                    fe-exec.c \
                    fe-gssapi-common.c \
                    fe-lobj.c \
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 100644
--- a/src/interfaces/libpq/test/Makefile
+++ b/src/interfaces/libpq/test/Makefile
@@ -15,10 +15,17 @@ override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
 LDFLAGS_INTERNAL += $(libpq_pgport)
 
 PROGS = libpq_testclient libpq_uri_regress
+ifeq ($(with_ssl),openssl)
+PROGS += libpq_test_encrypt
+endif
+
 
 all: $(PROGS)
 
 $(PROGS): $(WIN32RES)
 
+libpq_test_encrypt: ../fe-encrypt-openssl.c
+	$(CC) $(CPPFLAGS) -DTEST_ENCRYPT $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+
 clean distclean maintainer-clean:
 	rm -f $(PROGS) *.o
diff --git a/src/interfaces/libpq/test/meson.build b/src/interfaces/libpq/test/meson.build
index b2a4b06fd2..87d2808b52 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c9a3868053 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -24,7 +24,7 @@ ifeq ($(with_ldap),yes)
 SUBDIRS += ldap
 endif
 ifeq ($(with_ssl),openssl)
-SUBDIRS += ssl
+SUBDIRS += column_encryption ssl
 endif
 
 # Test suites that are not safe by default but can be run if selected
@@ -36,7 +36,7 @@ export PG_TEST_EXTRA
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),column_encryption examples kerberos icu ldap ssl)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..764cadf550
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/column_encryption/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+export OPENSSL
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/meson.build b/src/test/column_encryption/meson.build
new file mode 100644
index 0000000000..84cfa84e12
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,23 @@
+column_encryption_test_client = executable('test_client',
+  files('test_client.c'),
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+testprep_targets += column_encryption_test_client
+
+tests += {
+  'name': 'column_encryption',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_column_encryption.pl',
+      't/002_cmk_rotation.pl',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..8fa253524d
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,257 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+my $perlbin = $^X;
+$perlbin =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$openssl, 'pkeyutl', '-encrypt',
+		'-inkey', $cmkfilename,
+		'-pkeyopt', 'rsa_padding_mode:oaep',
+		'-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+		'-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+	# create CEK in database
+	$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = ${cmkname}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c smallint ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# Expected ciphertext length is 2 blocks of AES output (2 * 16) plus
+# half SHA-256 output (16) in hex encoding: (2 * 16 + 16) * 2 = 96
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1) TO STDOUT}),
+	qr/1\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text
+c|smallint),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result');
+
+{
+	local $ENV{PGCMKLOOKUP} = '*=run:broken %k %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = qq{*=run:"$perlbin" ./test_run_decrypt.pl %k %a %p};
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$node->command_fails_like(['test_client', 'test1'], qr/not encrypted/, 'test client fails because parameters not encrypted');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result after test client insert');
+
+$node->command_ok(['test_client', 'test2'], 'test client test 2');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3|33),
+	'decrypted query result after test client insert 2');
+
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1 WHERE a = 3) TO STDOUT}),
+	qr/3\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3upd|33),
+	'decrypted query result after update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+is($node->safe_psql('postgres', q{SELECT b, count(*) FROM tbl2 GROUP BY b ORDER BY 2}),
+	q(valB|1
+valA|2),
+	'group by deterministically encrypted column');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl3 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c text ENCRYPTED WITH (column_encryption_key = cek2, algorithm = 'AEAD_AES_192_CBC_HMAC_SHA_384')
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1
+2|valB2|valC2
+3|valB3|valC3),
+	'decrypted query result multiple keys after second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..14eafb8ec9
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+# Test column master key rotation.  First, we generate CMK1 and a CEK
+# encrypted with it.  Then we add a CMK2 and encrypt the CEK with it
+# as well.  (Recall that a CEK can be associated with multiple CMKs,
+# for this reason.  That's why pg_colenckeydata is split out from
+# pg_colenckey.)  Then we remove CMK1.  We test that we can get
+# decrypted query results at each step.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+	system_or_bail $openssl, 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+	$node->safe_psql('postgres', qq{CREATE COLUMN MASTER KEY ${cmkname}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# generate random bytes
+system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+# encrypt CEK using CMK
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk1filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# create CEK in database
+$node->safe_psql('postgres', qq{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = cmk1, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$ENV{PGCMKLOOKUP} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b) VALUES (1, $1) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with one CMK');
+
+
+# create new CMK
+my $cmk2filename = create_cmk('cmk2');
+
+# encrypt CEK using new CMK
+#
+# (Here, we still have the plaintext of the CEK available from
+# earlier.  In reality, one would decrypt the CEK with the first CMK
+# and then re-encrypt it with the second CMK.)
+system_or_bail $openssl, 'pkeyutl', '-encrypt',
+  '-inkey', $cmk2filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+$cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# add new data record for CEK in database
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} ADD VALUE (column_master_key = cmk2, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with two CMKs');
+
+
+# delete CEK record for first CMK
+$node->safe_psql('postgres', qq{ALTER COLUMN ENCRYPTION KEY ${cekname} DROP VALUE (column_master_key = cmk1);});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with only new CMK');
+
+
+done_testing();
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (!(!PQparamisencrypted(res2, 0) &&
+		  PQparamisencrypted(res2, 1)))
+	{
+		fprintf(stderr, "wrong results from PQparamisencrypted()\n");
+		return 1;
+	}
+
+	res = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	PGconn	   *conn;
+	int			ret = 0;
+
+	conn = PQconnectdb("");
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		fprintf(stderr, "Connection to database failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (argc < 2 || argv[1] == NULL)
+		return 87;
+	else if (strcmp(argv[1], "test1") == 0)
+		ret = test1(conn);
+	else if (strcmp(argv[1], "test2") == 0)
+		ret = test2(conn);
+	else if (strcmp(argv[1], "test3") == 0)
+		ret = test3(conn);
+	else if (strcmp(argv[1], "test4") == 0)
+		ret = test4(conn);
+	else
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+
+# Test/sample command for libpq cmklookup run scheme
+#
+# This just places the data into temporary files and runs the openssl
+# command on it.  (In practice, this could more simply be written as a
+# shell script, but this way it's more portable.)
+
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $fh, '<:raw', "${tmpdir}/output.tmp" or die $!;
+my $data = '';
+
+while (1) {
+	my $success = read $fh, $data, 100, length($data);
+	die $! if not defined $success;
+	last if not $success;
+}
+
+close $fh;
+
+unlink "${tmpdir}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..d55ddb1ab7 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,7 @@ subdir('subscription')
 subdir('modules')
 
 if ssl.found()
+  subdir('column_encryption')
   subdir('ssl')
 endif
 
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..df47e36280
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,451 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE ROLE regress_enc_user1;
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+CREATE COLUMN MASTER KEY cmk2;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "cek1" already has data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "fail" does not exist
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+ERROR:  column encryption key "notexist" does not exist
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+\d tbl_29f3
+              Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_29f3
+                                        Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+\d tbl_447f
+              Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_447f
+                                        Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  permission denied for column master key cmk1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+ERROR:  permission denied for column encryption key cek1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column master key cmk1 depends on column master key cmk1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ERROR:  column master key "cmk3" already exists in schema "public"
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+ERROR:  column master key "cmkx" does not exist
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ERROR:  column encryption key "cek3" already exists in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+ERROR:  must be owner of column encryption key cek3
+DROP COLUMN MASTER KEY cmk3;  -- fail
+ERROR:  must be owner of column master key cmk3
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ERROR:  column encryption key "cek1" has no data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ERROR:  column master key "fail" does not exist
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+ERROR:  attribute "algorithm" must not be specified
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+ERROR:  column encryption key "fail" does not exist
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf..8dbb4a847a 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -101,6 +103,7 @@ BEGIN
         ('materialized view'), ('foreign table'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
+        ('column encryption key'), ('column encryption key data'), ('column master key'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
         ('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
         ('text search parser'), ('text search dictionary'),
@@ -201,6 +204,24 @@ WARNING:  error for cast,{addr_nsp,zwei},{}: name list length must be exactly 1
 WARNING:  error for cast,{addr_nsp,zwei},{integer}: name list length must be exactly 1
 WARNING:  error for cast,{eins,zwei,drei},{}: name list length must be exactly 1
 WARNING:  error for cast,{eins,zwei,drei},{integer}: name list length must be exactly 1
+WARNING:  error for column encryption key,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins},{}: column master key "eins" does not exist
+WARNING:  error for column master key,{eins},{integer}: column master key "eins" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 WARNING:  error for table constraint,{eins},{}: must specify relation and object name
 WARNING:  error for table constraint,{eins},{integer}: must specify relation and object name
 WARNING:  error for table constraint,{addr_nsp,zwei},{}: relation "addr_nsp" does not exist
@@ -409,6 +430,9 @@ WITH objects (type, name, args) AS (VALUES
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,8 @@ drop cascades to user mapping for regress_addr_user on server integer
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 DROP SCHEMA addr_nsp CASCADE;
 NOTICE:  drop cascades to 14 other objects
 DETAIL:  drop cascades to text search dictionary addr_ts_dict
@@ -547,6 +576,9 @@ WITH objects (classid, objid, objsubid) AS (VALUES
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|NULL
+("(""column master key"",,,)")|("(""column master key"",,)")|NULL
+("(""column encryption key"",,,)")|("(""column encryption key"",,)")|NULL
+("(""column encryption key data"",,,)")|("(""column encryption key data"",,)")|NULL
 -- restore normal output mode
 \a\t
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..226f5e404e 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attusertypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,9 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,13 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
  anyrange                    | anymultirange
-(5 rows)
+(6 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -872,6 +876,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+pg_encrypted_det_eq(pg_encrypted_det,pg_encrypted_det)
+pg_encrypted_det_ne(pg_encrypted_det,pg_encrypted_det)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -75,7 +75,9 @@ ORDER BY t1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | pg_encrypted_det
+ 8244 | pg_encrypted_rnd
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT t1.oid, t1.typname as basetype, t2.typname as arraytype,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 15e015b3d6..577a6d4b2e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 427429975e..57c7d2bec3 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..8d6320015c
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,297 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE ROLE regress_enc_user1;
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk2;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+\d tbl_29f3
+\d+ tbl_29f3
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+
+\d tbl_447f
+\d+ tbl_447f
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+
+
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+
+
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..61828613d9 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -93,6 +95,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
         ('materialized view'), ('foreign table'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
+        ('column encryption key'), ('column encryption key data'), ('column master key'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
         ('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
         ('text search parser'), ('text search dictionary'),
@@ -174,6 +177,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('type', '{addr_nsp.genenum}', '{}'),
     ('cast', '{int8}', '{int4}'),
     ('collation', '{default}', '{}'),
+    ('column encryption key', '{addr_cek}', '{}'),
+    ('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+    ('column master key', '{addr_cmk}', '{}'),
     ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
     ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
     ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -228,6 +234,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 
 DROP SCHEMA addr_nsp CASCADE;
 
@@ -247,6 +255,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 36ea345f8fa616fd9b40576310e54145aa70c1a1
-- 
2.39.2



^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* Re: Transparent column encryption
@ 2023-03-11 18:08  Mark Dilger <[email protected]>
  parent: Peter Eisentraut <[email protected]>
  1 sibling, 0 replies; 62+ messages in thread

From: Mark Dilger @ 2023-03-11 18:08 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: pgsql-hackers



> On Mar 9, 2023, at 11:18 PM, Peter Eisentraut <[email protected]> wrote:
> 
> Here is an updated patch.

Thanks, Peter.  The patch appears to be in pretty good shape, but I have a few comments and concerns.

CEKIsVisible() and CMKIsVisible() are obviously copied from TSParserIsVisible(), and the code comments weren't fully updated.  Specifically, the phrase "hidden by another parser of the same name" should be updated to not mention "parser".

Why does get_cmkalg_name() return the string "unspecified" for PG_CMK_UNSPECIFIED, but the next function get_cmkalg_jwa_name() returns NULL for PG_CMK_UNSPECIFIED?  It seems they would both return NULL, or both return "unspecified".  If there's a reason for the divergence, could you add a code comment to clarify?  BTW, get_cmkalg_jwa_name() has no test coverage.
 
Looking further at code coverage, the new conditional in printsimple_startup() is never tested with (MyProcPort->column_encryption_enabled), so the block is never entered.  This would seem to be a consequence of backends like walsender not using column encryption, which is not terribly surprising, but it got me wondering if you had a particular client use case in mind when you added this block?

The new function pg_encrypted_in() appears totally untested, but I have to wonder if that's because we're not round-trip testing pg_dump with column encryption...?  The code coverage in pg_dump looks fairly decent, but some column encryption code is not covered.

—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company









^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* Re: Transparent column encryption
@ 2023-03-12 00:11  Andres Freund <[email protected]>
  parent: Peter Eisentraut <[email protected]>
  1 sibling, 1 reply; 62+ messages in thread

From: Andres Freund @ 2023-03-12 00:11 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: pgsql-hackers

Hi,

On 2023-03-10 08:18:34 +0100, Peter Eisentraut wrote:
> Here is an updated patch.  I have done some cosmetic polishing and fixed a
> minor Windows-related bug.
> 
> In my mind, the patch is complete.
> 
> If someone wants to do some in-depth code review, I suggest focusing on the
> following files:
> 
> * src/backend/access/common/printtup.c

Have you done benchmarks of some simple workloads to verify this doesn't cause
slowdowns (when not using encryption, obviously)? printtup.c is a performance
sensitive portion for simple queries, particularly when they return multiple
columns.

And making tupledescs even wider is likely to have some price, both due to the
increase in memory usage, and due to the lower cache density - and that's code
where we're already hurting noticeably.

Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* Re: Transparent column encryption
@ 2023-03-13 20:22  Peter Eisentraut <[email protected]>
  parent: Andres Freund <[email protected]>
  0 siblings, 1 reply; 62+ messages in thread

From: Peter Eisentraut @ 2023-03-13 20:22 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: pgsql-hackers

On 12.03.23 01:11, Andres Freund wrote:
> Have you done benchmarks of some simple workloads to verify this doesn't cause
> slowdowns (when not using encryption, obviously)? printtup.c is a performance
> sensitive portion for simple queries, particularly when they return multiple
> columns.

The additional code isn't used when column encryption is off, so there 
shouldn't be any impact.







^ permalink  raw  reply  [nested|flat] 62+ messages in thread

* Re: Transparent column encryption
@ 2023-03-13 20:41  Andres Freund <[email protected]>
  parent: Peter Eisentraut <[email protected]>
  0 siblings, 0 replies; 62+ messages in thread

From: Andres Freund @ 2023-03-13 20:41 UTC (permalink / raw)
  To: Peter Eisentraut <[email protected]>; +Cc: pgsql-hackers

Hi,

On 2023-03-13 21:22:29 +0100, Peter Eisentraut wrote:
> On 12.03.23 01:11, Andres Freund wrote:
> > Have you done benchmarks of some simple workloads to verify this doesn't cause
> > slowdowns (when not using encryption, obviously)? printtup.c is a performance
> > sensitive portion for simple queries, particularly when they return multiple
> > columns.
> 
> The additional code isn't used when column encryption is off, so there
> shouldn't be any impact.

It adds branches, and it makes tupledescs wider. In tight spots, such as
printtup, that can hurt, even if the branches aren't ever entered.

Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 62+ messages in thread


end of thread, other threads:[~2023-03-13 20:41 UTC | newest]

Thread overview: 62+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2020-05-29 05:39 [PATCH] Fast COPY FROM into the foreign (or sharded) table. Andrey V. Lepikhov <[email protected]>
2020-05-29 05:39 [PATCH] Fast COPY FROM into the foreign (or sharded) table. Andrey V. Lepikhov <[email protected]>
2020-06-17 06:07 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-06-22 05:28 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey V. Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-07-09 06:16 [PATCH] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-09-10 09:21 [PATCH 2/4] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-09-20 08:44 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey V. Lepikhov <[email protected]>
2020-10-19 11:24 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey V. Lepikhov <[email protected]>
2020-11-10 17:55 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey V. Lepikhov <[email protected]>
2020-12-14 08:37 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2020-12-14 08:37 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-01-12 03:54 [PATCH 2/2] Fast COPY FROM into the foreign or sharded table. Andrey Lepikhov <[email protected]>
2021-02-09 03:50 [PATCH] Fast COPY FROM into the foreign or sharded table. Takayuki Tsunakawa <[email protected]>
2023-02-22 10:25 Re: Transparent column encryption Peter Eisentraut <[email protected]>
2023-02-28 20:28 ` Re: Transparent column encryption Peter Eisentraut <[email protected]>
2023-03-10 07:18   ` Re: Transparent column encryption Peter Eisentraut <[email protected]>
2023-03-11 18:08     ` Re: Transparent column encryption Mark Dilger <[email protected]>
2023-03-12 00:11     ` Re: Transparent column encryption Andres Freund <[email protected]>
2023-03-13 20:22       ` Re: Transparent column encryption Peter Eisentraut <[email protected]>
2023-03-13 20:41         ` Re: Transparent column encryption Andres Freund <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox