public inbox for [email protected]  
help / color / mirror / Atom feed
From: Peter Eisentraut <[email protected]>
To: pgsql-hackers <[email protected]>
Subject: Transparent column encryption
Date: Fri, 3 Dec 2021 22:32:21 +0100
Message-ID: <[email protected]> (raw)

I want to present my proof-of-concept patch for the transparent column
encryption feature.  (Some might also think of it as automatic
client-side encryption or similar, but I like my name.)  This feature
enables the {automatic,transparent} 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
the "prying eyes" of DBAs, sysadmins, cloud operators, 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.  Of course, you 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 you can do equality lookups, at the cost of some
security.

This functionality also exists in other SQL database products, so the
overall concepts weren't invented by me by any means.

Also, this feature has nothing to do with the on-disk encryption
feature being contemplated in parallel.  Both can exist independently.

The attached patch has all the necessary pieces in place to make this
work, so you can have an idea how the overall system works.  It
contains some documentation and tests to help illustrate the
functionality.  But it's missing the remaining 90% of the work,
including additional DDL support, error handling, robust memory
management, protocol versioning, forward and backward compatibility,
pg_dump support, psql \d support, refinement of the cryptography, and
so on.  But I think obvious solutions exist to all of those things, so
it isn't that interesting to focus on them for now.

------

Now to the explanation of how it works.

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 assymmetric 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, the catalog object specifies a "provider" and generic
options.  Right now, libpq has a "file" provider hardcoded, and it
takes a "filename" option.  Via some mechanism to be determined,
additional providers could be loaded and then talk to key management
systems via http or whatever.  I have left some comments in the libpq
code where the hook points for this could be.

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.)

The encryption algorithms are mostly hardcoded right now, but there
are facilities for picking algorithms and adding new ones that will be
expanded.  The CMK process uses RSA-OAEP.  The CEK process uses
AES-128-CBC right now; a more complete solution should probably
involve some HMAC thrown in.

In the server, the encrypted datums are stored in types called
encryptedr and encryptedd (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, encryptedr has no operators at all,
encryptedd 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.

Some protocol extensions are required.  These should be guarded by
some _pq_... setting, but this is not done in this patch yet.  As
mentioned above, extra messages are added for sending the CMKs and
CEKs.  In the RowDescription message, I have commandeered the format
field to add a bit that indicates that the field is encrypted.  This
could be made a separate field, and there should probably be
additional fields to indicate the algorithm and CEK name, but this was
easiest for now.  The ParameterDescription message is extended to
contain format fields for each parameter, for the same purpose.
Again, this could be done differently.

Speaking of parameter descriptions, the trickiest part of this whole
thing appears to be how to get transparently encrypted data into the
database (as opposed to reading it out).  It is required to use
protocol-level prepared statements (i.e., extended query) for this.
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.  So this will require some care by applications
that want to do this, but, well, they probably should be careful
anyway.  In libpq, the existing APIs make this difficult, because
there is no way to pass the result of a describe-statement call back
into execute-statement-with-parameters.  I added new functions that do
this, so you then essentially do

     res0 = PQdescribePrepared(conn, "");
     res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, 0, res0);

(The name could obviously be improved.)  Other client APIs that have a
"statement handle" concept could do this more elegantly and probably
without any API changes.

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.  The current
implementation of this works for the test cases, but I know it has
some problems, so I'll continue working in this.  This functionality
is in principle available to all prepared-statement variants, not only
protocol-level.  So you can see in the tests that I expanded the
pg_prepared_statements view to show this information as well, which
also provides an easy way to test and debug this functionality
independent of column encryption.

And also, psql doesn't use prepared statements, so writing into
encrypted columns currently doesn't work at all via psql.  (Reading
works no problem.)  All the test code currently uses custom libpq C
programs.  We should think about a way to enable prepared statements
in psql, perhaps something like

INSERT INTO t1 VALUES ($1, $2) \gg 'val1' 'val2'

(\gexec and \gx are already taken.)

------

This is not targeting PostgreSQL 15.  But I'd appreciate some feedback
on the direction.  As I mentioned above, a lot of the remaining work
is arguably mostly straightforward.  Some closer examination of the
issues surrounding the libpq API changes and psql would be useful.
Perhaps there are other projects where that kind of functionality
would also be useful.
From 84281a522bedc1b83bf4fbea5390b8b1f1591152 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Fri, 3 Dec 2021 22:07:52 +0100
Subject: [PATCH v1] Transparent column encryption

---
 doc/src/sgml/catalogs.sgml                    | 227 +++++++++++
 doc/src/sgml/datatype.sgml                    |  47 +++
 doc/src/sgml/ddl.sgml                         |  65 ++++
 doc/src/sgml/libpq.sgml                       |  79 ++++
 doc/src/sgml/protocol.sgml                    | 220 ++++++++++-
 doc/src/sgml/ref/create_table.sgml            |  42 ++-
 src/backend/access/common/printtup.c          | 150 ++++++++
 src/backend/access/common/tupdesc.c           |   8 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/heap.c                    |   3 +
 src/backend/commands/prepare.c                |  51 ++-
 src/backend/commands/tablecmds.c              |  62 +++
 src/backend/executor/spi.c                    |   4 +
 src/backend/nodes/copyfuncs.c                 |   1 +
 src/backend/nodes/equalfuncs.c                |   1 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/nodes/outfuncs.c                  |   1 +
 src/backend/parser/analyze.c                  |   5 +-
 src/backend/parser/gram.y                     |  13 +-
 src/backend/parser/parse_param.c              |  43 ++-
 src/backend/parser/parse_target.c             |   6 +
 src/backend/tcop/postgres.c                   |  58 ++-
 src/backend/utils/cache/plancache.c           |  10 +
 src/backend/utils/cache/syscache.c            |  29 ++
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |   9 +
 src/include/catalog/pg_cast.dat               |   6 +
 src/include/catalog/pg_colenckey.h            |  18 +
 src/include/catalog/pg_colenckeydata.h        |  25 ++
 src/include/catalog/pg_colmasterkey.h         |  24 ++
 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               |  13 +-
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/analyze.h                  |   3 +-
 src/include/parser/parse_node.h               |   2 +
 src/include/parser/parse_param.h              |   3 +-
 src/include/utils/plancache.h                 |   4 +
 src/include/utils/syscache.h                  |   3 +
 src/interfaces/libpq/exports.txt              |   2 +
 src/interfaces/libpq/fe-connect.c             |   5 +
 src/interfaces/libpq/fe-exec.c                | 352 +++++++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           |  83 ++++-
 src/interfaces/libpq/fe-trace.c               |   2 +
 src/interfaces/libpq/libpq-fe.h               |  16 +
 src/interfaces/libpq/libpq-int.h              |  13 +
 src/test/Makefile                             |   3 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  20 +
 .../t/001_column_encryption.pl                | 135 +++++++
 src/test/column_encryption/test_client.c      | 155 ++++++++
 .../libpq_pipeline/traces/prepared.trace      |   2 +-
 .../regress/expected/column_encryption.out    |  47 +++
 src/test/regress/expected/oidjoins.out        |   4 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/prepare.out         |  41 +-
 src/test/regress/expected/rules.out           |   6 +-
 src/test/regress/expected/sanity_check.out    |   3 +
 src/test/regress/expected/type_sanity.out     |  20 +-
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/sql/column_encryption.sql    |  44 +++
 src/test/regress/sql/prepare.sql              |   6 +-
 src/test/regress/sql/type_sanity.sql          |   4 +-
 69 files changed, 2196 insertions(+), 67 deletions(-)
 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/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/test_client.c
 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/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1d11be73f..a220aa4dd2 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>
@@ -2423,6 +2438,218 @@ <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>
+    </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>text</type>
+      </para>
+      <para>
+       The encryption algorithm used for encrypting the key material
+      </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>cmkprovider</structfield> <type>text</type>
+      </para>
+      <para>
+       The provider associated with this column master key.  This is used by
+       clients to determine how to look up the key.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkoptions</structfield> <type>text[]</type>
+      </para>
+      <para>
+       Provider-specific options, such as a file name or a URL.
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 6929f3bb18..bcda249179 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5341,4 +5341,51 @@ <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>encryptedr</type> (for randomized encryption) or
+    <type>encryptedd</type> (for deterministic encryption); see <xref
+    linkend="datatype-encrypted-table"/>.  Most of the database system treats
+    this as normal types.  For example, the type <type>encryptedd</type> has
+    an equals operator that allows lookup of encrypted values.  The external
+    representation of these types is the same as the <type>bytea</type> type.
+    Thus, clients that don't support transparent column encryption or have
+    disabled it will see the encrypted values as byte arrays.  Clients that
+    support transparent data encryption will not see these types in result
+    sets, as the protocol layer will translate them back to 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>encryptedd</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, deterministic encryption</entry>
+       </row>
+       <row>
+        <entry><type>encryptedr</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 642ea2a70d..b0c26958a7 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1161,6 +1161,71 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Transparent Column Encryption</title>
+
+  <para>
+   With <firstterm>transparent column encryption</firstterm>, columns can be
+   stored encrypted in the database.  The encryption and decryption happens 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>
+
+  <para>
+   Tranparent column 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 assymmetric 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> 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 to make
+   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>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index c17d33a54f..f1bb5e9df5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -3012,6 +3012,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPrepared2">
+      <term><function>PQexecPrepared2</function><indexterm><primary>PQexecPrepared2</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 *PQexecPrepared2(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-PQexecPrepared2"/> 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>
 
@@ -4563,6 +4601,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPrepared2"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4570,6 +4609,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPrepared2"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4681,6 +4721,44 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPrepared2">
+     <term><function>PQsendQueryPrepared2</function><indexterm><primary>PQsendQueryPrepared2</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 PQsendQueryPrepared(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-PQsendQueryPrepared2"/> 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.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4731,6 +4809,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPrepared2"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 34a7034282..9cb8aad8d6 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1050,6 +1050,47 @@ <title>Extended Query</title>
    </note>
   </sect2>
 
+  <sect2>
+   <title>Transparent Column Encryption</title>
+
+   <para>
+    When transparent column 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 a name
+    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 transparent column 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, the plaintext is always in text format
+    (not binary format).
+   </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>
+  </sect2>
+
   <sect2>
    <title>Function Call</title>
 
@@ -4083,6 +4124,159 @@ <title>Message Formats</title>
 </varlistentry>
 
 
+<varlistentry>
+<term>
+ColumnEncryptionKey (B)
+</term>
+<listitem>
+<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>
+        String
+</term>
+<listitem>
+<para>
+                The name of the 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.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+
+</para>
+</listitem>
+</varlistentry>
+
+
+<varlistentry>
+<term>
+ColumnMasterKey (B)
+</term>
+<listitem>
+<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>
+        String
+</term>
+<listitem>
+<para>
+                The name of the key.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+        String
+</term>
+<listitem>
+<para>
+                The name of the key provider.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+        Int16
+</term>
+<listitem>
+<para>
+                The number of options to follow.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+        Next, the following fields follow for each option:
+<variablelist>
+<varlistentry>
+<term>
+        String
+</term>
+<listitem>
+<para>
+                Option name
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+        String
+</term>
+<listitem>
+<para>
+                Option value
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+
+</para>
+</listitem>
+</varlistentry>
+
+
 <varlistentry>
 <term>
 CommandComplete (B)
@@ -5408,6 +5602,23 @@ <title>Message Formats</title>
 </para>
 </listitem>
 </varlistentry>
+</variablelist>
+        And then, for each parameter, there is the following:
+<variablelist>
+<varlistentry>
+<term>
+        Int16
+</term>
+<listitem>
+<para>
+                A format code.  This is used as a bit field.  If bit 0x10 is
+                set, the parameter corresponds to an encrypted column and must
+                be sent encrypted.  If bit 0x20 is set in addition, the column
+                uses deterministic encryption, otherwise randomized
+                encryption.
+</para>
+</listitem>
+</varlistentry>
 </variablelist>
 </para>
 </listitem>
@@ -5876,10 +6087,11 @@ <title>Message Formats</title>
 </term>
 <listitem>
 <para>
-                The format code being used for the field.  Currently will
-                be zero (text) or one (binary).  In a RowDescription
-                returned from the statement variant of Describe, the
-                format code is not yet known and will always be zero.
+                The format code being used for the field.  The lower byte will
+                be zero (text) or one (binary).  The upper byte will be 1 if
+                the column is encrypted.  In a RowDescription returned from
+                the statement variant of Describe, the format code is not yet
+                known, so the lower byte will always be zero.
 </para>
 </listitem>
 </varlistentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 57d51a676a..d72043d2b7 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> [ 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> [ COMPRESSION <replaceable>compression_method</replaceable> ] [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -317,6 +317,46 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables transparent column 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.
+         </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>AES-CBC-128</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 54b539f6fb..19a27325d8 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,25 @@
  */
 #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/pqformat.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.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 +163,127 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 	 */
 }
 
+List *cmk_sent = NIL;
+
+static void
+MaybeSendColumnMasterKeyMessage(Oid cmkid)
+{
+	HeapTuple	tuple;
+	Form_pg_colmasterkey cmkform;
+	Datum		datum;
+	bool		isnull;
+	int			n;
+	AnyArrayType *arr;
+	array_iter	iter;
+	StringInfoData buf;
+	MemoryContext oldcontext;
+
+	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_sendstring(&buf, NameStr(cmkform->cmkname));
+
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkprovider, &isnull);
+	Assert(!isnull);
+	pq_sendstring(&buf, TextDatumGetCString(datum));
+
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkoptions, &isnull);
+	Assert(!isnull);
+	arr = DatumGetAnyArrayP(datum);
+	Assert(ARR_NDIM((ArrayType *) arr) == 1);
+	n = ARR_DIMS((ArrayType *) arr)[0];
+	Assert(n % 2 == 0);
+	pq_sendint16(&buf, n / 2);
+
+	array_iter_setup(&iter, arr);
+
+	for (int i = 0; i < n; i++)
+	{
+		datum = array_iter_next(&iter, &isnull, i, -1, false, 'i');
+		Assert(!isnull);
+		pq_sendstring(&buf, TextDatumGetCString(datum));
+	}
+
+	pq_endmessage(&buf);
+
+	ReleaseSysCache(tuple);
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cmk_sent = lappend_oid(cmk_sent, cmkid);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+List *cek_sent = NIL;
+
+void
+MaybeSendColumnEncryptionKeyMessage(Oid attcek)
+{
+	HeapTuple	tuple;
+	HeapTuple	tuple2;
+	Form_pg_colenckey cekform;
+	ScanKeyData	skey[1];
+	SysScanDesc	sd;
+	Relation	rel;
+	bool		found = false;
+	MemoryContext oldcontext;
+
+	if (list_member_oid(cek_sent, attcek))
+		return;
+
+	tuple = SearchSysCache1(CEKOID, ObjectIdGetDatum(attcek));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+	cekform = (Form_pg_colenckey) GETSTRUCT(tuple);
+
+	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 ((tuple2 = systable_getnext(sd)))
+	{
+		Form_pg_colenckeydata ckdform = (Form_pg_colenckeydata) GETSTRUCT(tuple2);
+		Datum		datum;
+		bool		isnull;
+		bytea	   *ba;
+		StringInfoData buf;
+
+		MaybeSendColumnMasterKeyMessage(ckdform->ckdcmkid);
+
+		datum = heap_getattr(tuple2, Anum_pg_colenckeydata_ckdencval, RelationGetDescr(rel), &isnull);
+		Assert(!isnull);
+		ba = pg_detoast_datum_packed((bytea *) DatumGetPointer(datum));
+
+		pq_beginmessage(&buf, 'Y'); /* ColumnEncryptionKey */
+		pq_sendstring(&buf, NameStr(cekform->cekname));
+		pq_sendint32(&buf, VARSIZE_ANY_EXHDR(ba));
+		pq_sendbytes(&buf, VARDATA_ANY(ba), VARSIZE_ANY_EXHDR(ba));
+		pq_endmessage(&buf);
+
+		found = true;
+	}
+
+	if (!found)
+		elog(ERROR, "lookup failed for column encryption key data %u", attcek);
+
+	systable_endscan(sd);
+	table_close(rel, NoLock);
+
+	ReleaseSysCache(tuple);
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cek_sent = lappend_oid(cek_sent, attcek);
+	MemoryContextSwitchTo(oldcontext);
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -231,6 +364,23 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (get_typtype(atttypid) == TYPTYPE_ENCRYPTED)
+		{
+			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->attrealtypid;
+
+			ReleaseSysCache(tp);
+
+			format |= 0x10;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 9b506471fe..602e9e10a7 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,10 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attrealtypid != attr2->attrealtypid)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +633,8 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +696,8 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
 	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 1e343df0af..3c3dee5c36 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 == ENCRYPTEDDOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 4e6efda97f..85d6ee76e6 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -69,7 +69,8 @@ CATALOG_HEADERS := \
 	pg_default_acl.h pg_init_privs.h pg_seclabel.h pg_shseclabel.h \
 	pg_collation.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/heap.c b/src/backend/catalog/heap.c
index 8bbf23e452..3b8e0b92a1 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -792,6 +792,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_attrealtypid - 1] = ObjectIdGetDatum(attrs->attrealtypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attencalg - 1] = Int32GetDatum(attrs->attencalg);
 		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)
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5e03c7c5aa..7925007c41 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -61,6 +61,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	RawStmt    *rawstmt;
 	CachedPlanSource *plansource;
 	Oid		   *argtypes = NULL;
+	Oid		   *argorigtbls = NULL;
+	AttrNumber *argorigcols = NULL;
 	int			nargs;
 	Query	   *query;
 	List	   *query_list;
@@ -108,6 +110,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 
 			argtypes[i++] = toid;
 		}
+
+		argorigtbls = (Oid *) palloc0(nargs * sizeof(Oid));
+		argorigcols = (AttrNumber *) palloc0(nargs * sizeof(AttrNumber));
 	}
 
 	/*
@@ -116,7 +121,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 */
 	query = parse_analyze_varparams(rawstmt, pstate->p_sourcetext,
-									&argtypes, &nargs);
+									&argtypes, &nargs, &argorigtbls, &argorigcols);
 
 	/*
 	 * Check that all parameter types were determined.
@@ -159,6 +164,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 					   NULL,
 					   argtypes,
 					   nargs,
+					   argorigtbls,
+					   argorigcols,
 					   NULL,
 					   NULL,
 					   CURSOR_OPT_PARALLEL_OK,	/* allow parallel mode */
@@ -724,7 +731,7 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 	 * build tupdesc for result tuples. This must match the definition of the
 	 * pg_prepared_statements view in system_views.sql
 	 */
-	tupdesc = CreateTemplateTupleDesc(7);
+	tupdesc = CreateTemplateTupleDesc(9);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "name",
 					   TEXTOID, -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "statement",
@@ -739,6 +746,10 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 					   INT8OID, -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 7, "custom_plans",
 					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 8, "parameter_orig_tables",
+					   REGCLASSARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 9, "parameter_orig_columns",
+					   INT2ARRAYOID, -1, 0);
 
 	/*
 	 * We put all the tuples into a tuplestore in one scan of the hashtable.
@@ -760,8 +771,8 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 		hash_seq_init(&hash_seq, prepared_queries);
 		while ((prep_stmt = hash_seq_search(&hash_seq)) != NULL)
 		{
-			Datum		values[7];
-			bool		nulls[7];
+			Datum		values[9];
+			bool		nulls[9];
 
 			MemSet(nulls, 0, sizeof(nulls));
 
@@ -773,6 +784,38 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 			values[4] = BoolGetDatum(prep_stmt->from_sql);
 			values[5] = Int64GetDatumFast(prep_stmt->plansource->num_generic_plans);
 			values[6] = Int64GetDatumFast(prep_stmt->plansource->num_custom_plans);
+			{
+				int			num_params = prep_stmt->plansource->num_params;
+				Datum      *tmp_ary;
+				ArrayType  *result;
+				int         i;
+
+				tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+
+				for (i = 0; i < num_params; i++)
+					tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_origtbls[i]);
+
+				/* XXX: this hardcodes assumptions about the regclass type */
+				result = construct_array(tmp_ary, num_params, REGCLASSOID,
+										 4, true, TYPALIGN_INT);
+				values[7] = PointerGetDatum(result);
+			}
+			{
+				int			num_params = prep_stmt->plansource->num_params;
+				Datum      *tmp_ary;
+				ArrayType  *result;
+				int         i;
+
+				tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+
+				for (i = 0; i < num_params; i++)
+					tmp_ary[i] = Int16GetDatum(prep_stmt->plansource->param_origcols[i]);
+
+				/* XXX: this hardcodes assumptions about the int2 type */
+				result = construct_array(tmp_ary, num_params, INT2OID,
+										 2, true, TYPALIGN_SHORT);
+				values[8] = PointerGetDatum(result);
+			}
 
 			tuplestore_putvalues(tupstore, tupdesc, values, nulls);
 		}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c35f09998c..8dea6fafd3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -33,6 +33,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
@@ -900,6 +901,64 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 		if (colDef->compression)
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
+
+		if (colDef->encryption)
+		{
+			ListCell *lc;
+			char *cek = NULL;
+			Oid cekoid;
+			bool encdet = false;
+			int alg = 1;
+
+			foreach(lc, colDef->encryption)
+			{
+				DefElem *el = lfirst_node(DefElem, lc);
+
+				if (strcmp(el->defname, "column_encryption_key") == 0)
+					cek = strVal(linitial(castNode(TypeName, el->arg)->names));
+				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
+						elog(ERROR, "unrecognized encryption type: %s", val);
+				}
+				else if (strcmp(el->defname, "algorithm") == 0)
+				{
+					char *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+					if (strcmp(val, "AES-CBC-128") == 0)
+						alg = 1;
+					if (strcmp(val, "AES-CBC-192") == 0)
+						alg = 2;
+					if (strcmp(val, "AES-CBC-256") == 0)
+						alg = 3;
+					else
+						elog(ERROR, "unrecognized encryption algorithm: %s", val);
+				}
+				else
+					elog(ERROR, "unrecognized column encryption parameter: %s", el->defname);
+			}
+
+			if (!cek)
+				elog(ERROR, "column encryption key must be specified");
+
+			cekoid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid, PointerGetDatum(cek));
+			if (!cekoid)
+				elog(ERROR, "column encryption key \"%s\" does not exist", cek);
+
+			attr->attcek = cekoid;
+			attr->attrealtypid = attr->atttypid;
+			if (encdet)
+				attr->atttypid = ENCRYPTEDDOID;
+			else
+				attr->atttypid = ENCRYPTEDROID;
+			attr->attencalg = alg;
+		}
 	}
 
 	/*
@@ -6758,6 +6817,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attgenerated = colDef->generated;
 	attribute.attisdropped = false;
 	attribute.attislocal = colDef->is_local;
+	attribute.attcek = 0; // TODO
+	attribute.attrealtypid = 0; // TODO
+	attribute.attencalg = 0; // TODO
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
 
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 0568ae123f..b1bfe12eab 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2186,6 +2186,8 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan)
 						   NULL,
 						   plan->argtypes,
 						   plan->nargs,
+						   NULL,
+						   NULL,
 						   plan->parserSetup,
 						   plan->parserSetupArg,
 						   plan->cursor_options,
@@ -2423,6 +2425,8 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 							   NULL,
 							   plan->argtypes,
 							   plan->nargs,
+							   NULL,
+							   NULL,
 							   plan->parserSetup,
 							   plan->parserSetupArg,
 							   plan->cursor_options,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 297b6ee715..4f6255baa9 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3035,6 +3035,7 @@ _copyColumnDef(const ColumnDef *from)
 	COPY_STRING_FIELD(colname);
 	COPY_NODE_FIELD(typeName);
 	COPY_STRING_FIELD(compression);
+	COPY_NODE_FIELD(encryption);
 	COPY_SCALAR_FIELD(inhcount);
 	COPY_SCALAR_FIELD(is_local);
 	COPY_SCALAR_FIELD(is_not_null);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f537d3eb96..cf0a23a2c4 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2679,6 +2679,7 @@ _equalColumnDef(const ColumnDef *a, const ColumnDef *b)
 	COMPARE_STRING_FIELD(colname);
 	COMPARE_NODE_FIELD(typeName);
 	COMPARE_STRING_FIELD(compression);
+	COMPARE_NODE_FIELD(encryption);
 	COMPARE_SCALAR_FIELD(inhcount);
 	COMPARE_SCALAR_FIELD(is_local);
 	COMPARE_SCALAR_FIELD(is_not_null);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index e276264882..33621141f1 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3917,6 +3917,8 @@ raw_expression_tree_walker(Node *node,
 					return true;
 				if (walker(coldef->compression, context))
 					return true;
+				if (walker(coldef->encryption, context))
+					return true;
 				if (walker(coldef->raw_default, context))
 					return true;
 				if (walker(coldef->collClause, context))
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 3600c9fa36..ecd2e86abf 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2953,6 +2953,7 @@ _outColumnDef(StringInfo str, const ColumnDef *node)
 	WRITE_STRING_FIELD(colname);
 	WRITE_NODE_FIELD(typeName);
 	WRITE_STRING_FIELD(compression);
+	WRITE_NODE_FIELD(encryption);
 	WRITE_INT_FIELD(inhcount);
 	WRITE_BOOL_FIELD(is_local);
 	WRITE_BOOL_FIELD(is_not_null);
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 146ee8dd1e..411c474a89 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -148,7 +148,8 @@ parse_analyze(RawStmt *parseTree, const char *sourceText,
  */
 Query *
 parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
-						Oid **paramTypes, int *numParams)
+						Oid **paramTypes, int *numParams,
+						Oid **paramOrigTbls, AttrNumber **paramOrigCols)
 {
 	ParseState *pstate = make_parsestate(NULL);
 	Query	   *query;
@@ -158,7 +159,7 @@ parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
 
 	pstate->p_sourcetext = sourceText;
 
-	parse_variable_parameters(pstate, paramTypes, numParams);
+	parse_variable_parameters(pstate, paramTypes, numParams, paramOrigTbls, paramOrigCols);
 
 	query = transformTopLevelStmt(pstate, parseTree);
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 86ce33bd97..449dc4e9b1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -568,6 +568,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
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_actions key_delete key_match key_update key_action
@@ -3476,12 +3477,13 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_compression opt_column_encryption create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 					n->colname = $1;
 					n->typeName = $2;
 					n->compression = $3;
+					n->encryption = $4;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3490,8 +3492,8 @@ columnDef:	ColId Typename opt_column_compression create_generic_options ColQualL
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $4;
-					SplitColQualList($5, &n->constraints, &n->collClause,
+					n->fdwoptions = $5;
+					SplitColQualList($6, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *)n;
@@ -3546,6 +3548,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 ColQualList:
 			ColQualList ColConstraint				{ $$ = lappend($1, $2); }
 			| /*EMPTY*/								{ $$ = NIL; }
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index 68a5534393..4fd5cb1443 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -49,6 +49,8 @@ typedef struct VarParamState
 {
 	Oid		  **paramTypes;		/* array of parameter type OIDs */
 	int		   *numParams;		/* number of array entries */
+	Oid		  **paramOrigTbls;
+	AttrNumber **paramOrigCols;
 } VarParamState;
 
 static Node *fixed_paramref_hook(ParseState *pstate, ParamRef *pref);
@@ -56,6 +58,7 @@ static Node *variable_paramref_hook(ParseState *pstate, ParamRef *pref);
 static Node *variable_coerce_param_hook(ParseState *pstate, Param *param,
 										Oid targetTypeId, int32 targetTypeMod,
 										int location);
+static void variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 static bool check_parameter_resolution_walker(Node *node, ParseState *pstate);
 static bool query_contains_extern_params_walker(Node *node, void *context);
 
@@ -81,15 +84,19 @@ parse_fixed_parameters(ParseState *pstate,
  */
 void
 parse_variable_parameters(ParseState *pstate,
-						  Oid **paramTypes, int *numParams)
+						  Oid **paramTypes, int *numParams,
+						  Oid **paramOrigTbls, AttrNumber **paramOrigCols)
 {
 	VarParamState *parstate = palloc(sizeof(VarParamState));
 
 	parstate->paramTypes = paramTypes;
 	parstate->numParams = numParams;
+	parstate->paramOrigTbls = paramOrigTbls;
+	parstate->paramOrigCols = paramOrigCols;
 	pstate->p_ref_hook_state = (void *) parstate;
 	pstate->p_paramref_hook = variable_paramref_hook;
 	pstate->p_coerce_param_hook = variable_coerce_param_hook;
+	pstate->p_param_assign_orig_hook = variable_param_assign_orig_hook;
 }
 
 /*
@@ -145,14 +152,36 @@ variable_paramref_hook(ParseState *pstate, ParamRef *pref)
 	{
 		/* Need to enlarge param array */
 		if (*parstate->paramTypes)
+		{
 			*parstate->paramTypes = (Oid *) repalloc(*parstate->paramTypes,
 													 paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc(*parstate->paramOrigTbls,
+													paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc(*parstate->paramOrigCols,
+													paramno * sizeof(AttrNumber));
+		}
 		else
+		{
 			*parstate->paramTypes = (Oid *) palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc(paramno * sizeof(AttrNumber));
+		}
 		/* Zero out the previously-unreferenced slots */
 		MemSet(*parstate->paramTypes + *parstate->numParams,
 			   0,
 			   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigTbls)
+			MemSet(*parstate->paramOrigTbls + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigCols)
+			MemSet(*parstate->paramOrigCols + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(AttrNumber));
 		*parstate->numParams = paramno;
 	}
 
@@ -260,6 +289,18 @@ variable_coerce_param_hook(ParseState *pstate, Param *param,
 	return NULL;
 }
 
+static void
+variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol)
+{
+	VarParamState *parstate = (VarParamState *) pstate->p_ref_hook_state;
+	int			paramno = param->paramid;
+
+	if (parstate->paramOrigTbls)
+		(*parstate->paramOrigTbls)[paramno - 1] = origtbl;
+	if (parstate->paramOrigCols)
+		(*parstate->paramOrigCols)[paramno - 1] = origcol;
+}
+
 /*
  * Check for consistent assignment of variable parameters after completion
  * of parsing with parse_variable_parameters.
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 9ce3a0de96..5d0093d7ac 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -595,6 +595,12 @@ transformAssignedExpr(ParseState *pstate,
 					 parser_errposition(pstate, exprLocation(orig_expr))));
 	}
 
+	if (IsA(expr, Param))
+	{
+		if (pstate->p_param_assign_orig_hook)
+			pstate->p_param_assign_orig_hook(pstate, castNode(Param, expr), RelationGetRelid(pstate->p_target_relation), attrno);
+	}
+
 	pstate->p_expr_kind = sv_expr_kind;
 
 	return expr;
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 82de01cdc6..da7ab9d68b 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -80,6 +80,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"
 
@@ -1336,6 +1337,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	bool		is_named;
 	bool		save_log_statement_stats = log_statement_stats;
 	char		msec_str[32];
+	Oid		   *paramOrigTbls = palloc(numParams * sizeof(Oid));
+	AttrNumber *paramOrigCols = palloc(numParams * sizeof(AttrNumber));
 
 	/*
 	 * Report query to various monitoring facilities.
@@ -1459,7 +1462,9 @@ exec_parse_message(const char *query_string,	/* string to execute */
 		query = parse_analyze_varparams(raw_parse_tree,
 										query_string,
 										&paramTypes,
-										&numParams);
+										&numParams,
+										&paramOrigTbls,
+										&paramOrigCols);
 
 		/*
 		 * Check all parameter types got determined.
@@ -1508,6 +1513,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 					   unnamed_stmt_context,
 					   paramTypes,
 					   numParams,
+					   paramOrigTbls,
+					   paramOrigCols,
 					   NULL,
 					   NULL,
 					   CURSOR_OPT_PARALLEL_OK,	/* allow parallel mode */
@@ -1805,6 +1812,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter corresponds to an encrypted column, but the parameter value was not encrypted")));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2602,8 +2619,47 @@ exec_describe_statement_message(const char *stmt_name)
 	{
 		Oid			ptype = psrc->param_types[i];
 
+		if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+		{
+			Oid			porigtbl = psrc->param_origtbls[i];
+			AttrNumber	porigcol = psrc->param_origcols[i];
+			HeapTuple   tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol <= 0)
+				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)));
+
+			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);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			ptype = orig_att->attrealtypid;
+
+			ReleaseSysCache(tp);
+		}
+
 		pq_sendint32(&row_description_buf, (int) ptype);
 	}
+
+	for (int i = 0; i < psrc->num_params; i++)
+	{
+		int16		pformat;
+
+		if (get_typtype(psrc->param_types[i]) == TYPTYPE_ENCRYPTED)
+		{
+			pformat = 0x10;
+			if (psrc->param_types[i] == ENCRYPTEDDOID)
+				pformat |= 0x20;
+		}
+		else
+			pformat = 0;
+
+		pq_sendint16(&row_description_buf, pformat);
+	}
+
 	pq_endmessage_reuse(&row_description_buf);
 
 	/*
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 6767eae8f2..869f37b26e 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -340,6 +340,8 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 				   MemoryContext querytree_context,
 				   Oid *param_types,
 				   int num_params,
+				   Oid *param_origtbls,
+				   AttrNumber *param_origcols,
 				   ParserSetupHook parserSetup,
 				   void *parserSetupArg,
 				   int cursor_options,
@@ -419,6 +421,14 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 	{
 		plansource->param_types = (Oid *) palloc(num_params * sizeof(Oid));
 		memcpy(plansource->param_types, param_types, num_params * sizeof(Oid));
+
+		plansource->param_origtbls = (Oid *) palloc0(num_params * sizeof(Oid));
+		if (param_origtbls)
+			memcpy(plansource->param_origtbls, param_origtbls, num_params * sizeof(Oid));
+
+		plansource->param_origcols = (AttrNumber *) palloc0(num_params * sizeof(AttrNumber));
+		if (param_origcols)
+			memcpy(plansource->param_origcols, param_origcols, num_params * sizeof(AttrNumber));
 	}
 	else
 		plansource->param_types = NULL;
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 56870b46e4..5c3f9b0055 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -29,7 +29,9 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.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"
@@ -266,6 +268,24 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		256
 	},
+	{
+		ColumnEncKeyRelationId,	/* CEKNAME */
+		ColumnEncKeyNameIndexId,
+		1,
+		{
+			Anum_pg_colenckey_cekname,
+		},
+		8
+	},
+	{
+		ColumnEncKeyRelationId,	/* CEKOID */
+		ColumnEncKeyOidIndexId,
+		1,
+		{
+			Anum_pg_colenckey_oid,
+		},
+		8
+	},
 	{OperatorClassRelationId,	/* CLAAMNAMENSP */
 		OpclassAmNameNspIndexId,
 		3,
@@ -288,6 +308,15 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
+	{
+		ColumnMasterKeyRelationId,	/* CMKOID */
+		ColumnMasterKeyOidIndexId,
+		1,
+		{
+			Anum_pg_colmasterkey_oid,
+		},
+		8
+	},
 	{CollationRelationId,		/* COLLNAMEENCNSP */
 		CollationNameEncNspIndexId,
 		3,
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index c9b37538a0..90d8c93564 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/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 8135854163..815cdac0af 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' },
 
+# encryptedd_ops
+{ amopfamily => 'hash/encryptedd_ops', amoplefttype => 'encryptedd',
+  amoprighttype => 'encryptedd', amopstrategy => '1', amopopr => '=(encryptedd,encryptedd)',
+  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 5460aa2422..a3698eea0f 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/encryptedd_ops', amproclefttype => 'encryptedd',
+  amprocrighttype => 'encryptedd', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/encryptedd_ops', amproclefttype => 'encryptedd',
+  amprocrighttype => 'encryptedd', 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 5c1ec9313e..48c774c053 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,15 @@ 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);
+
+	/* real type if encrypted */
+	Oid			attrealtypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+
+	/* encryption algorithm */
+	int32		attencalg BKI_DEFAULT(0);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index 67f73cb6fb..22ff9d5c0b 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -548,4 +548,10 @@
 { castsource => 'tstzrange', casttarget => 'tstzmultirange',
   castfunc => 'tstzmultirange(tstzrange)', castcontext => 'e',
   castmethod => 'f' },
+
+{ castsource => 'bytea', casttarget => 'encryptedd', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+{ castsource => 'bytea', casttarget => 'encryptedr', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+
 ]
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..a59abc3a16
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,18 @@
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_index, 8242, ColumnEncKeyNameIndexId, on pg_colenckey using btree(cekname name_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..cd19b0501b
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,25 @@
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+#ifdef CATALOG_VARLEN           /* variable-length fields start here */
+	text		ckdcmkalg BKI_FORCE_NOT_NULL;
+	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..15292b0b61
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,24 @@
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+#ifdef CATALOG_VARLEN           /* variable-length fields start here */
+	text		cmkprovider BKI_FORCE_NOT_NULL;
+	text		cmkoptions[1] BKI_FORCE_NOT_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_index, 8241, ColumnMasterKeyNameIndexId, on pg_colmasterkey using btree(cmkname name_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 484727a2fc..d41e2aac2a 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 => 'encryptedd_ops', opcfamily => 'hash/encryptedd_ops',
+  opcintype => 'encryptedd' },
 { 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 0075a02f32..9fb9890522 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3475,4 +3475,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'encryptedd',
+  oprright => 'encryptedd', oprresult => 'bool', oprcom => '=(encryptedd,encryptedd)',
+  oprnegate => '<>(encryptedd,encryptedd)', oprcode => 'encrypteddeq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'encryptedd', oprright => 'encryptedd', oprresult => 'bool',
+  oprcom => '<>(encryptedd,encryptedd)', oprnegate => '=(encryptedd,encryptedd)',
+  oprcode => 'encrypteddne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 8e480efd28..dcf9839e42 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 => 'encryptedd_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 79d787cd26..44a822aca5 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7992,9 +7992,9 @@
   proname => 'pg_prepared_statement', prorows => '1000', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'record',
   proargtypes => '',
-  proallargtypes => '{text,text,timestamptz,_regtype,bool,int8,int8}',
-  proargmodes => '{o,o,o,o,o,o,o}',
-  proargnames => '{name,statement,prepare_time,parameter_types,from_sql,generic_plans,custom_plans}',
+  proallargtypes => '{text,text,timestamptz,_regtype,bool,int8,int8,_regclass,_int2}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o}',
+  proargnames => '{name,statement,prepare_time,parameter_types,from_sql,generic_plans,custom_plans,parameter_orig_tables,parameter_orig_columns}',
   prosrc => 'pg_prepared_statement' },
 { oid => '2511', descr => 'get the open cursors for this session',
   proname => 'pg_cursor', prorows => '1000', proretset => 't',
@@ -11733,4 +11733,11 @@
   prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary',
   prosrc => 'brin_minmax_multi_summary_send' },
 
+{ oid => '8245',
+  proname => 'encrypteddeq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'encryptedd encryptedd', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'encrypteddne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'encryptedd encryptedd', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 41074c994b..36e2e2c263 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 => 'encryptedd', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'x' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'encryptedr', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'x' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index e568e21dee..999720fb54 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -277,6 +277,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPTYPE_MULTIRANGE	'm' /* multirange type */
 #define  TYPTYPE_PSEUDO		'p' /* pseudo-type */
 #define  TYPTYPE_RANGE		'r' /* range type */
+#define  TYPTYPE_ENCRYPTED	'y'	/* encrypted column value */
 
 #define  TYPCATEGORY_INVALID	'\0'	/* not an allowed category */
 #define  TYPCATEGORY_ARRAY		'A'
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 067138e6b5..04e6e310ed 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -670,6 +670,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? */
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index a0f0bd38d7..c1b8f7617a 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -27,7 +27,8 @@ extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook;
 extern Query *parse_analyze(RawStmt *parseTree, const char *sourceText,
 							Oid *paramTypes, int numParams, QueryEnvironment *queryEnv);
 extern Query *parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
-									  Oid **paramTypes, int *numParams);
+									  Oid **paramTypes, int *numParams,
+									  Oid **paramOrigTbls, AttrNumber **paramOrigCols);
 
 extern Query *parse_sub_analyze(Node *parseTree, ParseState *parentParseState,
 								CommonTableExpr *parentCTE,
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index ee179082ce..011162a63f 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -92,6 +92,7 @@ typedef Node *(*ParseParamRefHook) (ParseState *pstate, ParamRef *pref);
 typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param,
 								  Oid targetTypeId, int32 targetTypeMod,
 								  int location);
+typedef void (*ParamAssignOrigHook) (ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 
 
 /*
@@ -221,6 +222,7 @@ struct ParseState
 	PostParseColumnRefHook p_post_columnref_hook;
 	ParseParamRefHook p_paramref_hook;
 	CoerceParamHook p_coerce_param_hook;
+	ParamAssignOrigHook p_param_assign_orig_hook;
 	void	   *p_ref_hook_state;	/* common passthrough link for above */
 };
 
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index b42fff296c..b90a315e9c 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -18,7 +18,8 @@
 extern void parse_fixed_parameters(ParseState *pstate,
 								   Oid *paramTypes, int numParams);
 extern void parse_variable_parameters(ParseState *pstate,
-									  Oid **paramTypes, int *numParams);
+									  Oid **paramTypes, int *numParams,
+									  Oid **paramOrigTbls, AttrNumber **paramOrigCols);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index ff09c63a02..eac90aa821 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -101,6 +101,8 @@ typedef struct CachedPlanSource
 	CommandTag	commandTag;		/* 'nuff said */
 	Oid		   *param_types;	/* array of parameter type OIDs, or NULL */
 	int			num_params;		/* length of param_types array */
+	Oid		   *param_origtbls;
+	AttrNumber *param_origcols;
 	ParserSetupHook parserSetup;	/* alternative parameter spec method */
 	void	   *parserSetupArg;
 	int			cursor_options; /* cursor options used for planning */
@@ -199,6 +201,8 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 							   MemoryContext querytree_context,
 							   Oid *param_types,
 							   int num_params,
+							   Oid *param_origtbls,
+							   AttrNumber *param_origcols,
 							   ParserSetupHook parserSetup,
 							   void *parserSetupArg,
 							   int cursor_options,
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index c8cfbc30f6..2420c9a818 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,11 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKNAME,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..7dacc93e0b 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,5 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPrepared2           187
+PQsendQueryPrepared2      188
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 9b6a6939f0..bc1b492557 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -4151,6 +4151,11 @@ freePGconn(PGconn *conn)
 		free(conn->gsslib);
 	if (conn->connip)
 		free(conn->connip);
+	if (conn->cekdata)
+	{
+		explicit_bzero(conn->cekdata, conn->cekdatalen);
+		free(conn->cekdata);
+	}
 	/* Note that conn->Pfdebug is not ours to close or free */
 	if (conn->write_err_msg)
 		free(conn->write_err_msg);
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 6c7b3df012..0e9431f360 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -28,6 +28,9 @@
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
 
+/* TODO: move OpenSSL-specific parts to separate file */
+#include <openssl/evp.h>
+
 /* keep this in same order as ExecStatusType in libpq-fe.h */
 char	   *const pgresStatus[] = {
 	"PGRES_EMPTY_QUERY",
@@ -65,7 +68,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);
@@ -1089,6 +1093,113 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+void
+pqSaveColumnMasterKey(PGconn *conn, const char *name, const char *provider,
+								  const char *optname1, const char *optval1)
+{
+	free(conn->cmkname);
+	conn->cmkname = strdup(name);
+	free(conn->cmkprovider);
+	conn->cmkprovider = strdup(provider);
+	free(conn->cmkoptname1);
+	conn->cmkoptname1 = strdup(optname1);
+	free(conn->cmkoptval1);
+	conn->cmkoptval1 = strdup(optval1);
+}
+
+/*
+ * a different key provider would replace this function
+ */
+static unsigned char *
+decrypt_cek_file(PGconn *conn,
+				 int fromlen, const unsigned char *from,
+				 int *tolen)
+{
+	RSA *rsa;
+	char *cmkfilename = conn->cmkoptval1;
+	FILE *fp;
+	unsigned char *to;
+	int rv;
+
+	/*
+	 * setup, load keys
+	 */
+
+	if (!cmkfilename)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("no CMK file set\n"));
+		goto fail;
+	}
+	rsa = RSA_new();
+	fp = fopen(cmkfilename, "rb");
+	if (!fp)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("could not open file \"%s\": %m\n"), cmkfilename);
+		goto fail;
+	}
+	rsa = PEM_read_RSAPrivateKey(fp, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("PEM_read_RSAPrivateKey() failed: %s\n"),
+						  ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	fclose(fp);
+
+	/*
+	 * decrypt
+	 */
+
+	to = malloc(RSA_size(rsa));
+
+	rv = RSA_private_decrypt(fromlen, from, to, rsa, RSA_PKCS1_OAEP_PADDING);
+	if (rv < 0)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("RSA_private_decrypt() failed: %s\n"),
+						  ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	RSA_free(rsa);
+
+	*tolen = rv;
+	return to;
+
+fail:
+	return false;
+}
+
+static unsigned char *
+decrypt_cek(PGconn *conn,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	if (strcmp(conn->cmkprovider, "file") == 0)
+		return decrypt_cek_file(conn, fromlen, from, tolen);
+	else
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("CMK provider \"%s\" not recognized\n"), conn->cmkprovider);
+		return NULL;
+	}
+}
+
+void
+pqSaveColumnEncryptionKey(PGconn *conn, const char *name, const unsigned char *value, int len)
+{
+	unsigned char *newval = NULL;
+	int newvallen = 0;
+
+	newval = decrypt_cek(conn, len, value, &newvallen);
+
+	free(conn->cekdata);
+	conn->cekdata = newval;
+	conn->cekdatalen = newvallen;
+}
 
 /*
  * pqRowProcessor
@@ -1153,9 +1264,103 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 		}
 		else
 		{
-			bool		isbinary = (res->attDescs[i].format != 0);
+			bool		isbinary = ((res->attDescs[i].format & 0x0F) != 0);
+			bool		isencrypted = ((res->attDescs[i].format & 0xF0) != 0);
 			char	   *val;
 
+			if (isencrypted)
+			{
+				unsigned char *v;
+				size_t l;
+				unsigned char *iv;
+				size_t ivlen;
+
+				EVP_CIPHER_CTX *evp_ctx;
+				const EVP_CIPHER *cipher;
+				unsigned char *decr;
+				int decrlen, decrlen2;
+
+				cipher = EVP_get_cipherbyname("AES-128-CBC");
+				if (!cipher)
+				{
+					*errmsgp = libpq_gettext("EVP_get_cipherbyname() failed");
+					goto fail;
+				}
+
+				evp_ctx = EVP_CIPHER_CTX_new();
+
+				if (!isbinary)
+				{
+					unsigned char *x;
+					x = malloc(clen + 1);
+					memcpy(x, columns[i].value, clen);
+					x[clen] = '\0';
+					v = PQunescapeBytea(x, &l);
+				}
+				else
+				{
+					v = unconstify(unsigned char *, (const unsigned char *) columns[i].value);
+					l = clen;
+				}
+
+				if (!EVP_DecryptInit_ex(evp_ctx, cipher, NULL, NULL, NULL))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptInit_ex() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+
+				if (conn->cekdatalen != EVP_CIPHER_CTX_key_length(evp_ctx))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("column encryption key has wrong key length for cipher (has: %zu, required: %d)"),
+							 conn->cekdatalen, EVP_CIPHER_CTX_key_length(evp_ctx));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+
+				ivlen = EVP_CIPHER_CTX_iv_length(evp_ctx);
+				iv = v;
+				v += ivlen;
+				l -= ivlen;
+				if (!EVP_DecryptInit_ex(evp_ctx, NULL, NULL, conn->cekdata, iv))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptInit_ex() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+
+				decr = malloc(l + EVP_CIPHER_CTX_block_size(evp_ctx) + 1);
+				if (!EVP_DecryptUpdate(evp_ctx, decr, &decrlen, v, l))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptUpdate() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+				if (!EVP_DecryptFinal_ex(evp_ctx, decr + decrlen, &decrlen2))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptFinal_ex() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+				decrlen += decrlen2;
+				decr[decrlen] = '\0';
+				val = (char *) decr;
+				clen = decrlen + 1;
+
+				EVP_CIPHER_CTX_free(evp_ctx);
+				free(iv);
+			}
+			else
+			{
 			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
 			if (val == NULL)
 				goto fail;
@@ -1163,6 +1368,7 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			/* 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;
@@ -1470,7 +1676,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1588,6 +1795,19 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPrepared2(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+int
+PQsendQueryPrepared2(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;
@@ -1615,7 +1835,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1712,7 +1933,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1760,13 +1982,24 @@ 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;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->format & 0x10)
+					format |= 0x10;
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1785,6 +2018,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1803,8 +2037,93 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].format & 0x10)
+			{
+				bool enc_det = (paramDesc->paramDescs[i].format & 0x20) != 0;
+				unsigned char *v;
+				unsigned char *iv;
+				size_t ivlen;
+				EVP_CIPHER_CTX *evp_ctx;
+				const EVP_CIPHER *cipher;
+				unsigned char *encr;
+				int encrlen, encrlen2;
+				unsigned char *esc;
+				size_t esclen;
+
+				v = unconstify(unsigned char *, (const unsigned char *) paramValue);
+
+				cipher = EVP_get_cipherbyname("AES-128-CBC");
+				if (!cipher)
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_get_cipherbyname() failed\n"));
+					goto sendFailed;
+				}
+
+				evp_ctx = EVP_CIPHER_CTX_new();
+
+				if (!EVP_EncryptInit_ex(evp_ctx, cipher, NULL, NULL, NULL))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptInit_ex() failed: %s\n"),
+									  ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+
+				if (conn->cekdatalen != EVP_CIPHER_CTX_key_length(evp_ctx))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("column encryption key has wrong key length for cipher (has: %zu, required: %d)"),
+									  conn->cekdatalen, EVP_CIPHER_CTX_key_length(evp_ctx));
+					goto sendFailed;
+				}
+
+				ivlen = EVP_CIPHER_CTX_iv_length(evp_ctx);
+				iv = malloc(ivlen);
+				if (enc_det)
+					memset(iv, ivlen, 0);
+				else
+					pg_strong_random(iv, ivlen);
+				if (!EVP_EncryptInit_ex(evp_ctx, NULL, NULL, conn->cekdata, iv))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptInit_ex() failed: %s\n"),
+									  ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+
+				encr = malloc(ivlen + nbytes + EVP_CIPHER_CTX_block_size(evp_ctx) + 1);
+				memcpy(encr, iv, ivlen);
+				encr += ivlen;
+				if (!EVP_EncryptUpdate(evp_ctx, encr, &encrlen, v, nbytes))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptUpdate() failed: %s\n"),
+													ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+				if (!EVP_EncryptFinal_ex(evp_ctx, encr + encrlen, &encrlen2))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptFinal_ex() failed: %s\n"),
+									  ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+				encrlen += encrlen2;
+
+				EVP_CIPHER_CTX_free(evp_ctx);
+				free(iv);
+
+				esc = PQescapeByteaConn(conn, encr - ivlen, encrlen + ivlen, &esclen);
+				nbytes = esclen - 1;
+				paramValue = (char *) esc;
+			}
+
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
 		}
 		else
@@ -2258,12 +2577,25 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPrepared2(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+PGresult *
+PQexecPrepared2(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,
+	if (!PQsendQueryPrepared2(conn, stmtName,
 							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+							 paramFormats, resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 9ab3bf1fcb..c40550cdf6 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -45,6 +45,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);
@@ -315,6 +317,14 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					if (getColumnMasterKey(conn))
+						return;
+					break;
+				case 'Y':		/* Column Encryption Key */
+					if (getColumnEncryptionKey(conn))
+						return;
+					break;
 				case 'T':		/* Row Description */
 					if (conn->result != NULL &&
 						conn->result->resultStatus == PGRES_FATAL_ERROR)
@@ -606,7 +616,7 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		result->attDescs[i].typlen = typlen;
 		result->attDescs[i].atttypmod = atttypmod;
 
-		if (format != 1)
+		if ((format & 0x0F) != 1)
 			result->binary = 0;
 	}
 
@@ -713,6 +723,14 @@ getParamDescriptions(PGconn *conn, int msgLength)
 			goto not_enough_data;
 		result->paramDescs[i].typid = typid;
 	}
+	for (i = 0; i < nparams; i++)
+	{
+		int			format;
+
+		if (pqGetInt(&format, 2, conn))
+			goto not_enough_data;
+		result->paramDescs[i].format = format;
+	}
 
 	/* Success! */
 	conn->result = result;
@@ -1429,6 +1447,69 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	char *keyname;
+	char *keyprov;
+	int noptions;
+	char *optname = NULL;
+	char *optval = NULL;
+
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	/* Get the key provider */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyprov = strdup(conn->workBuffer.data);
+	/* Get the options */
+	if (pqGetInt(&noptions, 2, conn) != 0)
+		return EOF;
+	for (int i = 0; i < noptions; i++)
+	{
+		if (pqGets(&conn->workBuffer, conn) != 0)
+			return EOF;
+		optname = strdup(conn->workBuffer.data);
+		if (pqGets(&conn->workBuffer, conn) != 0)
+			return EOF;
+		optval = strdup(conn->workBuffer.data);
+	}
+	/* And save it */
+	pqSaveColumnMasterKey(conn, keyname, keyprov, optname, optval);
+
+	free(keyname);
+	free(keyprov);
+	free(optname);
+	free(optval);
+	return 0;
+}
+
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	char *buf;
+	int vallen;
+
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, 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 (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	pqSaveColumnEncryptionKey(conn, conn->workBuffer.data, (unsigned char *) buf, vallen);
+	free(buf);
+	return 0;
+}
 
 /*
  * Attempt to read a Notify response message.
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index 8660d27926..e99a5d15af 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -459,6 +459,8 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 
 	for (int i = 0; i < nfields; i++)
 		pqTraceOutputInt32(f, message, cursor, regress);
+	for (int i = 0; i < nfields; i++)
+		pqTraceOutputInt16(f, message, cursor);
 }
 
 /* RowDescription */
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index a6fd69aceb..a6c8ec974a 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -436,6 +436,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPrepared2(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
@@ -459,6 +467,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPrepared2(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);
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 490458adef..aa0f2eec2c 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -111,6 +111,7 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	int			format;			/* encrypted parameter? */
 } PGresParamDesc;
 
 /*
@@ -475,6 +476,14 @@ struct pg_conn
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
 
+	/* Encryption stuff */
+	char	   *cmkname;
+	char	   *cmkprovider;
+	char	   *cmkoptname1;	/* XXX only one option right now */
+	char	   *cmkoptval1;
+	unsigned char *cekdata;		/* column encryption key (decrypted) */
+	size_t		cekdatalen;
+
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
 	int			inBufSize;		/* allocated size of buffer */
@@ -648,6 +657,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern void pqSaveColumnMasterKey(PGconn *conn, const char *name, const char *provider,
+								  const char *optname1, const char *optval1);
+extern void pqSaveColumnEncryptionKey(PGconn *conn, const char *name,
+									  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/test/Makefile b/src/test/Makefile
index 46275915ff..e2c819b047 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -28,6 +28,7 @@ SUBDIRS += ldap
 endif
 endif
 ifeq ($(with_ssl),openssl)
+SUBDIRS += column_encryption
 ifneq (,$(filter ssl,$(PG_TEST_EXTRA)))
 SUBDIRS += ssl
 endif
@@ -37,7 +38,7 @@ endif
 # 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 ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos 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..1b9a5c72be
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,20 @@
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+override CPPFLAGS += -I$(libpq_srcdir)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+EXTRA_INSTALL = contrib/pgcrypto
+
+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/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..b4eb7323b5
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,135 @@
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 13;
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+$node->safe_psql('postgres', qq{CREATE EXTENSION pgcrypto});
+
+my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/cmk1.pem";
+
+# generate CMK
+system_or_bail 'openssl', 'genrsa', '-out', $cmkfilename;
+
+# generate 16 random bytes for 128-bit key
+system_or_bail 'openssl', 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin", 16;
+
+# encrypt CEK using CMK
+system_or_bail 'openssl', 'rsautl', '-encrypt', '-oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin",
+  '-inkey', $cmkfilename,
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin.enc";
+
+# XXX might as well capture stdout, not go through file
+my $cekbin = slurp_file "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin";
+my $cekhex = unpack('H*', $cekbin);
+my $cekencbin = slurp_file "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin.enc";
+my $cekenchex = unpack('H*', $cekencbin);
+
+# create CMK in database
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkprovider, cmkoptions) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    'cmk1', 'file', '{"filename", "$cmkfilename"}'::text[]
+);
+});
+
+# create CEK in database
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckey (oid, cekname) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    'cek1'
+);
+});
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    'RSA-OAEP',
+    '\\x${cekenchex}'
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+my $ivhex = '00' x 16;
+$node->safe_psql('postgres', qq{
+INSERT INTO tbl1 (a, b) VALUES (1, ('\\x${ivhex}'::bytea || encrypt_iv('val1', '\\x${cekhex}', '\\x${ivhex}', 'aes'))::encryptedr);
+INSERT INTO tbl1 (a, b) VALUES (2, ('\\x${ivhex}'::bytea || encrypt_iv('val2', '\\x${cekhex}', '\\x${ivhex}', 'aes'))::encryptedr);
+});
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'decrypted query result');
+
+
+$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 FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'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 FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3),
+	'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\t\\\\x[0-9a-z]{32}/,
+	'inserted data is encrypted');
+
+$node->command_ok(['test_client', 'test3'], 'test client test 3');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3upd),
+	'decrypted query result after test client insert 3');
+
+$node->command_ok(['test_client', 'test4'], 'test client test 4');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result after test client insert 4');
+
+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');
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..be37b51305
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,155 @@
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3"};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					   2, NULL, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res, *res2;
+	const char *values[] = {"3", "val3"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					2, 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;
+	}
+
+	res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res, *res2;
+	const char *values[] = {"3", "val3upd"};
+
+	res = PQprepare(conn, "", "UPDATE tbl1 SET b = $2 WHERE a = $1",
+					2, 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;
+	}
+
+	res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res, *res2;
+	const char *values[] = {"1", "valA", "2", "valB", "3", "valA"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6)",
+					6, 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;
+	}
+
+	res = PQexecPrepared2(conn, "", 6, values, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	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/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace
index 1a7de5c3e6..ce84f1955b 100644
--- a/src/test/modules/libpq_pipeline/traces/prepared.trace
+++ b/src/test/modules/libpq_pipeline/traces/prepared.trace
@@ -2,7 +2,7 @@ F	68	Parse	 "select_one" "SELECT $1, '42', $1::numeric, interval '1 sec'" 1 NNNN
 F	16	Describe	 S "select_one"
 F	4	Sync
 B	4	ParseComplete
-B	10	ParameterDescription	 1 NNNN
+B	12	ParameterDescription	 1 NNNN 0
 B	113	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 65535 -1 0 "numeric" NNNN 0 NNNN 65535 -1 0 "interval" NNNN 0 NNNN 16 -1 0
 B	5	ReadyForQuery	 I
 F	10	Query	 "BEGIN"
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..1fec9f8903
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,47 @@
+/* Imagine:
+CREATE COLUMN MASTER KEY cmk1 (
+    provider = 'test'
+);
+*/
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkprovider, cmkoptions) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    'cmk1', 'test', '{}'::text[]
+);
+/* Imagine:
+CREATE COLUMN ENCRYPTION KEY cek1 (
+    column_master_key = cmk1,
+    algorithm = '...',
+    encrypted_value = '...'
+);
+*/
+INSERT INTO pg_colenckey (oid, cekname) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    'cek1'
+);
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    '2ROT13',
+    '\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_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      | encryptedr | default   |          | 
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..ab6405a6c3 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 {attrealtypid} => 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,5 @@ 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_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 562b586d8e..fc2a07971d 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -175,13 +175,14 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | encryptedd
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+(7 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,11 +198,12 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | encryptedd
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(4 rows)
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -840,6 +842,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+encrypteddeq(encryptedd,encryptedd)
+encrypteddne(encryptedd,encryptedd)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
@@ -987,7 +991,9 @@ WHERE c.castmethod = 'b' AND
  xml               | text              |        0 | a
  xml               | character varying |        0 | a
  xml               | character         |        0 | a
-(10 rows)
+ bytea             | encryptedd        |        0 | e
+ bytea             | encryptedr        |        0 | e
+(12 rows)
 
 -- **************** pg_conversion ****************
 -- Look for illegal values in pg_conversion fields.
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 3306c696b1..675556c7d1 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -159,25 +159,30 @@ PREPARE q6 AS
     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;
 PREPARE q7(unknown) AS
     SELECT * FROM road WHERE thepath = $1;
-SELECT name, statement, parameter_types FROM pg_prepared_statements
+-- DML statements
+PREPARE q8 AS
+    UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;
+SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns FROM pg_prepared_statements
     ORDER BY name;
- name |                            statement                             |                  parameter_types                   
-------+------------------------------------------------------------------+----------------------------------------------------
- q2   | PREPARE q2(text) AS                                             +| {text}
-      |         SELECT datname, datistemplate, datallowconn             +| 
-      |         FROM pg_database WHERE datname = $1;                     | 
- q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint}
-      |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+| 
-      |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +| 
-      |         ORDER BY unique1;                                        | 
- q5   | PREPARE q5(int, text) AS                                        +| {integer,text}
-      |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +| 
-      |         ORDER BY unique1;                                        | 
- q6   | PREPARE q6 AS                                                   +| {integer,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    | 
- q7   | PREPARE q7(unknown) AS                                          +| {path}
-      |     SELECT * FROM road WHERE thepath = $1;                       | 
-(5 rows)
+ name |                            statement                             |                  parameter_types                   | parameter_orig_tables | parameter_orig_columns 
+------+------------------------------------------------------------------+----------------------------------------------------+-----------------------+------------------------
+ q2   | PREPARE q2(text) AS                                             +| {text}                                             | {-}                   | {0}
+      |         SELECT datname, datistemplate, datallowconn             +|                                                    |                       | 
+      |         FROM pg_database WHERE datname = $1;                     |                                                    |                       | 
+ q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {-,-,-,-,-}           | {0,0,0,0,0}
+      |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    |                       | 
+      |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    |                       | 
+      |         ORDER BY unique1;                                        |                                                    |                       | 
+ q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {-,-}                 | {0,0}
+      |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    |                       | 
+      |         ORDER BY unique1;                                        |                                                    |                       | 
+ q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {-,-}                 | {0,0}
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    |                       | 
+ q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {-}                   | {0}
+      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    |                       | 
+ q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | {-,tenk1}             | {0,14}
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    |                       | 
+(6 rows)
 
 -- test DEALLOCATE ALL;
 DEALLOCATE ALL;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index b58b062b10..7883baf4ab 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1441,8 +1441,10 @@ pg_prepared_statements| SELECT p.name,
     p.parameter_types,
     p.from_sql,
     p.generic_plans,
-    p.custom_plans
-   FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, from_sql, generic_plans, custom_plans);
+    p.custom_plans,
+    p.parameter_orig_tables,
+    p.parameter_orig_columns
+   FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, from_sql, generic_plans, custom_plans, parameter_orig_tables, parameter_orig_columns);
 pg_prepared_xacts| SELECT p.transaction,
     p.gid,
     p.prepared,
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 63706a28cc..67a0fcc9a3 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -112,7 +112,10 @@ pg_auth_members|t
 pg_authid|t
 pg_cast|t
 pg_class|t
+pg_colenckey|t
+pg_colenckeydata|t
 pg_collation|t
+pg_colmasterkey|t
 pg_constraint|t
 pg_conversion|t
 pg_database|t
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 257b6cac12..403e9f5c1e 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -17,7 +17,7 @@ SELECT p1.oid, p1.typname
 FROM pg_type as p1
 WHERE p1.typnamespace = 0 OR
     (p1.typlen <= 0 AND p1.typlen != -1 AND p1.typlen != -2) OR
-    (p1.typtype not in ('b', 'c', 'd', 'e', 'p', 'r', 'm')) OR
+    (p1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT p1.typisdefined OR
     (p1.typalign not in ('c', 's', 'i', 'd')) OR
     (p1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -75,7 +75,9 @@ ORDER BY p1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | encryptedd
+ 8244 | encryptedr
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
@@ -210,7 +212,8 @@ ORDER BY 1;
  e       | enum_in
  m       | multirange_in
  r       | range_in
-(5 rows)
+ y       | byteain
+(6 rows)
 
 -- Check for bogus typoutput routines
 -- As of 8.0, this check finds refcursor, which is borrowing
@@ -255,7 +258,8 @@ ORDER BY 1;
  e       | enum_out
  m       | multirange_out
  r       | range_out
-(4 rows)
+ y       | byteaout
+(5 rows)
 
 -- Domains should have same typoutput as their base types
 SELECT p1.oid, p1.typname, p2.oid, p2.typname
@@ -335,7 +339,8 @@ ORDER BY 1;
  e       | enum_recv
  m       | multirange_recv
  r       | range_recv
-(5 rows)
+ y       | bytearecv
+(6 rows)
 
 -- Check for bogus typsend routines
 -- As of 7.4, this check finds refcursor, which is borrowing
@@ -380,7 +385,8 @@ ORDER BY 1;
  e       | enum_send
  m       | multirange_send
  r       | range_send
-(4 rows)
+ y       | byteasend
+(5 rows)
 
 -- Domains should have same typsend as their base types
 SELECT p1.oid, p1.typname, p2.oid, p2.typname
@@ -707,6 +713,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::encryptedr,
+  E'\\xDEADBEEF'::encryptedd,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 017e962fed..cfc7c4e69a 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,6 +123,9 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # ----------
 test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize
 
+# WIP
+test: column_encryption
+
 # event triggers cannot run concurrently with any test that runs DDL
 # oidjoins is read-only, though, and should run late for best coverage
 test: event_trigger oidjoins
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..745d6c2ede
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,44 @@
+/* Imagine:
+CREATE COLUMN MASTER KEY cmk1 (
+    provider = 'test'
+);
+*/
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkprovider, cmkoptions) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    'cmk1', 'test', '{}'::text[]
+);
+
+/* Imagine:
+CREATE COLUMN ENCRYPTION KEY cek1 (
+    column_master_key = cmk1,
+    algorithm = '...',
+    encrypted_value = '...'
+);
+*/
+INSERT INTO pg_colenckey (oid, cekname) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    'cek1'
+);
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    '2ROT13',
+    '\xDEADBEEF'
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+\d tbl_29f3
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index 985d0f05c9..b2aa96d370 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -71,7 +71,11 @@ CREATE TEMPORARY TABLE q5_prep_nodata AS EXECUTE q5(200, 'DTAAAA')
 PREPARE q7(unknown) AS
     SELECT * FROM road WHERE thepath = $1;
 
-SELECT name, statement, parameter_types FROM pg_prepared_statements
+-- DML statements
+PREPARE q8 AS
+    UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;
+
+SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns FROM pg_prepared_statements
     ORDER BY name;
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 8281076423..f31f9da139 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -20,7 +20,7 @@
 FROM pg_type as p1
 WHERE p1.typnamespace = 0 OR
     (p1.typlen <= 0 AND p1.typlen != -1 AND p1.typlen != -2) OR
-    (p1.typtype not in ('b', 'c', 'd', 'e', 'p', 'r', 'm')) OR
+    (p1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT p1.typisdefined OR
     (p1.typalign not in ('c', 's', 'i', 'd')) OR
     (p1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -529,6 +529,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::encryptedr,
+  E'\\xDEADBEEF'::encryptedd,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,

base-commit: 49422ad0cc88c91a38522b2a7b222c2f2c939f82
-- 
2.34.1



Attachments:

  [text/plain] v1-0001-Transparent-column-encryption.patch (123.7K, ../[email protected]/2-v1-0001-Transparent-column-encryption.patch)
  download | inline diff:
From 84281a522bedc1b83bf4fbea5390b8b1f1591152 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Fri, 3 Dec 2021 22:07:52 +0100
Subject: [PATCH v1] Transparent column encryption

---
 doc/src/sgml/catalogs.sgml                    | 227 +++++++++++
 doc/src/sgml/datatype.sgml                    |  47 +++
 doc/src/sgml/ddl.sgml                         |  65 ++++
 doc/src/sgml/libpq.sgml                       |  79 ++++
 doc/src/sgml/protocol.sgml                    | 220 ++++++++++-
 doc/src/sgml/ref/create_table.sgml            |  42 ++-
 src/backend/access/common/printtup.c          | 150 ++++++++
 src/backend/access/common/tupdesc.c           |   8 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/heap.c                    |   3 +
 src/backend/commands/prepare.c                |  51 ++-
 src/backend/commands/tablecmds.c              |  62 +++
 src/backend/executor/spi.c                    |   4 +
 src/backend/nodes/copyfuncs.c                 |   1 +
 src/backend/nodes/equalfuncs.c                |   1 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/nodes/outfuncs.c                  |   1 +
 src/backend/parser/analyze.c                  |   5 +-
 src/backend/parser/gram.y                     |  13 +-
 src/backend/parser/parse_param.c              |  43 ++-
 src/backend/parser/parse_target.c             |   6 +
 src/backend/tcop/postgres.c                   |  58 ++-
 src/backend/utils/cache/plancache.c           |  10 +
 src/backend/utils/cache/syscache.c            |  29 ++
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |   9 +
 src/include/catalog/pg_cast.dat               |   6 +
 src/include/catalog/pg_colenckey.h            |  18 +
 src/include/catalog/pg_colenckeydata.h        |  25 ++
 src/include/catalog/pg_colmasterkey.h         |  24 ++
 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               |  13 +-
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/analyze.h                  |   3 +-
 src/include/parser/parse_node.h               |   2 +
 src/include/parser/parse_param.h              |   3 +-
 src/include/utils/plancache.h                 |   4 +
 src/include/utils/syscache.h                  |   3 +
 src/interfaces/libpq/exports.txt              |   2 +
 src/interfaces/libpq/fe-connect.c             |   5 +
 src/interfaces/libpq/fe-exec.c                | 352 +++++++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           |  83 ++++-
 src/interfaces/libpq/fe-trace.c               |   2 +
 src/interfaces/libpq/libpq-fe.h               |  16 +
 src/interfaces/libpq/libpq-int.h              |  13 +
 src/test/Makefile                             |   3 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  20 +
 .../t/001_column_encryption.pl                | 135 +++++++
 src/test/column_encryption/test_client.c      | 155 ++++++++
 .../libpq_pipeline/traces/prepared.trace      |   2 +-
 .../regress/expected/column_encryption.out    |  47 +++
 src/test/regress/expected/oidjoins.out        |   4 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/prepare.out         |  41 +-
 src/test/regress/expected/rules.out           |   6 +-
 src/test/regress/expected/sanity_check.out    |   3 +
 src/test/regress/expected/type_sanity.out     |  20 +-
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/sql/column_encryption.sql    |  44 +++
 src/test/regress/sql/prepare.sql              |   6 +-
 src/test/regress/sql/type_sanity.sql          |   4 +-
 69 files changed, 2196 insertions(+), 67 deletions(-)
 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/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/test_client.c
 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/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1d11be73f..a220aa4dd2 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>
@@ -2423,6 +2438,218 @@ <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>
+    </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>text</type>
+      </para>
+      <para>
+       The encryption algorithm used for encrypting the key material
+      </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>cmkprovider</structfield> <type>text</type>
+      </para>
+      <para>
+       The provider associated with this column master key.  This is used by
+       clients to determine how to look up the key.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkoptions</structfield> <type>text[]</type>
+      </para>
+      <para>
+       Provider-specific options, such as a file name or a URL.
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 6929f3bb18..bcda249179 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5341,4 +5341,51 @@ <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>encryptedr</type> (for randomized encryption) or
+    <type>encryptedd</type> (for deterministic encryption); see <xref
+    linkend="datatype-encrypted-table"/>.  Most of the database system treats
+    this as normal types.  For example, the type <type>encryptedd</type> has
+    an equals operator that allows lookup of encrypted values.  The external
+    representation of these types is the same as the <type>bytea</type> type.
+    Thus, clients that don't support transparent column encryption or have
+    disabled it will see the encrypted values as byte arrays.  Clients that
+    support transparent data encryption will not see these types in result
+    sets, as the protocol layer will translate them back to 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>encryptedd</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, deterministic encryption</entry>
+       </row>
+       <row>
+        <entry><type>encryptedr</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 642ea2a70d..b0c26958a7 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1161,6 +1161,71 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Transparent Column Encryption</title>
+
+  <para>
+   With <firstterm>transparent column encryption</firstterm>, columns can be
+   stored encrypted in the database.  The encryption and decryption happens 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>
+
+  <para>
+   Tranparent column 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 assymmetric 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> 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 to make
+   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>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index c17d33a54f..f1bb5e9df5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -3012,6 +3012,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPrepared2">
+      <term><function>PQexecPrepared2</function><indexterm><primary>PQexecPrepared2</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 *PQexecPrepared2(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-PQexecPrepared2"/> 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>
 
@@ -4563,6 +4601,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPrepared2"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4570,6 +4609,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPrepared2"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4681,6 +4721,44 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPrepared2">
+     <term><function>PQsendQueryPrepared2</function><indexterm><primary>PQsendQueryPrepared2</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 PQsendQueryPrepared(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-PQsendQueryPrepared2"/> 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.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4731,6 +4809,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPrepared2"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 34a7034282..9cb8aad8d6 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1050,6 +1050,47 @@ <title>Extended Query</title>
    </note>
   </sect2>
 
+  <sect2>
+   <title>Transparent Column Encryption</title>
+
+   <para>
+    When transparent column 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 a name
+    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 transparent column 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, the plaintext is always in text format
+    (not binary format).
+   </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>
+  </sect2>
+
   <sect2>
    <title>Function Call</title>
 
@@ -4083,6 +4124,159 @@ <title>Message Formats</title>
 </varlistentry>
 
 
+<varlistentry>
+<term>
+ColumnEncryptionKey (B)
+</term>
+<listitem>
+<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>
+        String
+</term>
+<listitem>
+<para>
+                The name of the 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.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+
+</para>
+</listitem>
+</varlistentry>
+
+
+<varlistentry>
+<term>
+ColumnMasterKey (B)
+</term>
+<listitem>
+<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>
+        String
+</term>
+<listitem>
+<para>
+                The name of the key.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+        String
+</term>
+<listitem>
+<para>
+                The name of the key provider.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+        Int16
+</term>
+<listitem>
+<para>
+                The number of options to follow.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+        Next, the following fields follow for each option:
+<variablelist>
+<varlistentry>
+<term>
+        String
+</term>
+<listitem>
+<para>
+                Option name
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+        String
+</term>
+<listitem>
+<para>
+                Option value
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+
+</para>
+</listitem>
+</varlistentry>
+
+
 <varlistentry>
 <term>
 CommandComplete (B)
@@ -5408,6 +5602,23 @@ <title>Message Formats</title>
 </para>
 </listitem>
 </varlistentry>
+</variablelist>
+        And then, for each parameter, there is the following:
+<variablelist>
+<varlistentry>
+<term>
+        Int16
+</term>
+<listitem>
+<para>
+                A format code.  This is used as a bit field.  If bit 0x10 is
+                set, the parameter corresponds to an encrypted column and must
+                be sent encrypted.  If bit 0x20 is set in addition, the column
+                uses deterministic encryption, otherwise randomized
+                encryption.
+</para>
+</listitem>
+</varlistentry>
 </variablelist>
 </para>
 </listitem>
@@ -5876,10 +6087,11 @@ <title>Message Formats</title>
 </term>
 <listitem>
 <para>
-                The format code being used for the field.  Currently will
-                be zero (text) or one (binary).  In a RowDescription
-                returned from the statement variant of Describe, the
-                format code is not yet known and will always be zero.
+                The format code being used for the field.  The lower byte will
+                be zero (text) or one (binary).  The upper byte will be 1 if
+                the column is encrypted.  In a RowDescription returned from
+                the statement variant of Describe, the format code is not yet
+                known, so the lower byte will always be zero.
 </para>
 </listitem>
 </varlistentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 57d51a676a..d72043d2b7 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> [ 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> [ COMPRESSION <replaceable>compression_method</replaceable> ] [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -317,6 +317,46 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables transparent column 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.
+         </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>AES-CBC-128</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 54b539f6fb..19a27325d8 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,25 @@
  */
 #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/pqformat.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.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 +163,127 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 	 */
 }
 
+List *cmk_sent = NIL;
+
+static void
+MaybeSendColumnMasterKeyMessage(Oid cmkid)
+{
+	HeapTuple	tuple;
+	Form_pg_colmasterkey cmkform;
+	Datum		datum;
+	bool		isnull;
+	int			n;
+	AnyArrayType *arr;
+	array_iter	iter;
+	StringInfoData buf;
+	MemoryContext oldcontext;
+
+	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_sendstring(&buf, NameStr(cmkform->cmkname));
+
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkprovider, &isnull);
+	Assert(!isnull);
+	pq_sendstring(&buf, TextDatumGetCString(datum));
+
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkoptions, &isnull);
+	Assert(!isnull);
+	arr = DatumGetAnyArrayP(datum);
+	Assert(ARR_NDIM((ArrayType *) arr) == 1);
+	n = ARR_DIMS((ArrayType *) arr)[0];
+	Assert(n % 2 == 0);
+	pq_sendint16(&buf, n / 2);
+
+	array_iter_setup(&iter, arr);
+
+	for (int i = 0; i < n; i++)
+	{
+		datum = array_iter_next(&iter, &isnull, i, -1, false, 'i');
+		Assert(!isnull);
+		pq_sendstring(&buf, TextDatumGetCString(datum));
+	}
+
+	pq_endmessage(&buf);
+
+	ReleaseSysCache(tuple);
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cmk_sent = lappend_oid(cmk_sent, cmkid);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+List *cek_sent = NIL;
+
+void
+MaybeSendColumnEncryptionKeyMessage(Oid attcek)
+{
+	HeapTuple	tuple;
+	HeapTuple	tuple2;
+	Form_pg_colenckey cekform;
+	ScanKeyData	skey[1];
+	SysScanDesc	sd;
+	Relation	rel;
+	bool		found = false;
+	MemoryContext oldcontext;
+
+	if (list_member_oid(cek_sent, attcek))
+		return;
+
+	tuple = SearchSysCache1(CEKOID, ObjectIdGetDatum(attcek));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+	cekform = (Form_pg_colenckey) GETSTRUCT(tuple);
+
+	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 ((tuple2 = systable_getnext(sd)))
+	{
+		Form_pg_colenckeydata ckdform = (Form_pg_colenckeydata) GETSTRUCT(tuple2);
+		Datum		datum;
+		bool		isnull;
+		bytea	   *ba;
+		StringInfoData buf;
+
+		MaybeSendColumnMasterKeyMessage(ckdform->ckdcmkid);
+
+		datum = heap_getattr(tuple2, Anum_pg_colenckeydata_ckdencval, RelationGetDescr(rel), &isnull);
+		Assert(!isnull);
+		ba = pg_detoast_datum_packed((bytea *) DatumGetPointer(datum));
+
+		pq_beginmessage(&buf, 'Y'); /* ColumnEncryptionKey */
+		pq_sendstring(&buf, NameStr(cekform->cekname));
+		pq_sendint32(&buf, VARSIZE_ANY_EXHDR(ba));
+		pq_sendbytes(&buf, VARDATA_ANY(ba), VARSIZE_ANY_EXHDR(ba));
+		pq_endmessage(&buf);
+
+		found = true;
+	}
+
+	if (!found)
+		elog(ERROR, "lookup failed for column encryption key data %u", attcek);
+
+	systable_endscan(sd);
+	table_close(rel, NoLock);
+
+	ReleaseSysCache(tuple);
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cek_sent = lappend_oid(cek_sent, attcek);
+	MemoryContextSwitchTo(oldcontext);
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -231,6 +364,23 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (get_typtype(atttypid) == TYPTYPE_ENCRYPTED)
+		{
+			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->attrealtypid;
+
+			ReleaseSysCache(tp);
+
+			format |= 0x10;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 9b506471fe..602e9e10a7 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,10 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attrealtypid != attr2->attrealtypid)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +633,8 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +696,8 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
 	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 1e343df0af..3c3dee5c36 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 == ENCRYPTEDDOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 4e6efda97f..85d6ee76e6 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -69,7 +69,8 @@ CATALOG_HEADERS := \
 	pg_default_acl.h pg_init_privs.h pg_seclabel.h pg_shseclabel.h \
 	pg_collation.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/heap.c b/src/backend/catalog/heap.c
index 8bbf23e452..3b8e0b92a1 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -792,6 +792,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_attrealtypid - 1] = ObjectIdGetDatum(attrs->attrealtypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attencalg - 1] = Int32GetDatum(attrs->attencalg);
 		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)
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5e03c7c5aa..7925007c41 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -61,6 +61,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	RawStmt    *rawstmt;
 	CachedPlanSource *plansource;
 	Oid		   *argtypes = NULL;
+	Oid		   *argorigtbls = NULL;
+	AttrNumber *argorigcols = NULL;
 	int			nargs;
 	Query	   *query;
 	List	   *query_list;
@@ -108,6 +110,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 
 			argtypes[i++] = toid;
 		}
+
+		argorigtbls = (Oid *) palloc0(nargs * sizeof(Oid));
+		argorigcols = (AttrNumber *) palloc0(nargs * sizeof(AttrNumber));
 	}
 
 	/*
@@ -116,7 +121,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 */
 	query = parse_analyze_varparams(rawstmt, pstate->p_sourcetext,
-									&argtypes, &nargs);
+									&argtypes, &nargs, &argorigtbls, &argorigcols);
 
 	/*
 	 * Check that all parameter types were determined.
@@ -159,6 +164,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 					   NULL,
 					   argtypes,
 					   nargs,
+					   argorigtbls,
+					   argorigcols,
 					   NULL,
 					   NULL,
 					   CURSOR_OPT_PARALLEL_OK,	/* allow parallel mode */
@@ -724,7 +731,7 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 	 * build tupdesc for result tuples. This must match the definition of the
 	 * pg_prepared_statements view in system_views.sql
 	 */
-	tupdesc = CreateTemplateTupleDesc(7);
+	tupdesc = CreateTemplateTupleDesc(9);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "name",
 					   TEXTOID, -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "statement",
@@ -739,6 +746,10 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 					   INT8OID, -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 7, "custom_plans",
 					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 8, "parameter_orig_tables",
+					   REGCLASSARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 9, "parameter_orig_columns",
+					   INT2ARRAYOID, -1, 0);
 
 	/*
 	 * We put all the tuples into a tuplestore in one scan of the hashtable.
@@ -760,8 +771,8 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 		hash_seq_init(&hash_seq, prepared_queries);
 		while ((prep_stmt = hash_seq_search(&hash_seq)) != NULL)
 		{
-			Datum		values[7];
-			bool		nulls[7];
+			Datum		values[9];
+			bool		nulls[9];
 
 			MemSet(nulls, 0, sizeof(nulls));
 
@@ -773,6 +784,38 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 			values[4] = BoolGetDatum(prep_stmt->from_sql);
 			values[5] = Int64GetDatumFast(prep_stmt->plansource->num_generic_plans);
 			values[6] = Int64GetDatumFast(prep_stmt->plansource->num_custom_plans);
+			{
+				int			num_params = prep_stmt->plansource->num_params;
+				Datum      *tmp_ary;
+				ArrayType  *result;
+				int         i;
+
+				tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+
+				for (i = 0; i < num_params; i++)
+					tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_origtbls[i]);
+
+				/* XXX: this hardcodes assumptions about the regclass type */
+				result = construct_array(tmp_ary, num_params, REGCLASSOID,
+										 4, true, TYPALIGN_INT);
+				values[7] = PointerGetDatum(result);
+			}
+			{
+				int			num_params = prep_stmt->plansource->num_params;
+				Datum      *tmp_ary;
+				ArrayType  *result;
+				int         i;
+
+				tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+
+				for (i = 0; i < num_params; i++)
+					tmp_ary[i] = Int16GetDatum(prep_stmt->plansource->param_origcols[i]);
+
+				/* XXX: this hardcodes assumptions about the int2 type */
+				result = construct_array(tmp_ary, num_params, INT2OID,
+										 2, true, TYPALIGN_SHORT);
+				values[8] = PointerGetDatum(result);
+			}
 
 			tuplestore_putvalues(tupstore, tupdesc, values, nulls);
 		}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c35f09998c..8dea6fafd3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -33,6 +33,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
@@ -900,6 +901,64 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 		if (colDef->compression)
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
+
+		if (colDef->encryption)
+		{
+			ListCell *lc;
+			char *cek = NULL;
+			Oid cekoid;
+			bool encdet = false;
+			int alg = 1;
+
+			foreach(lc, colDef->encryption)
+			{
+				DefElem *el = lfirst_node(DefElem, lc);
+
+				if (strcmp(el->defname, "column_encryption_key") == 0)
+					cek = strVal(linitial(castNode(TypeName, el->arg)->names));
+				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
+						elog(ERROR, "unrecognized encryption type: %s", val);
+				}
+				else if (strcmp(el->defname, "algorithm") == 0)
+				{
+					char *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+					if (strcmp(val, "AES-CBC-128") == 0)
+						alg = 1;
+					if (strcmp(val, "AES-CBC-192") == 0)
+						alg = 2;
+					if (strcmp(val, "AES-CBC-256") == 0)
+						alg = 3;
+					else
+						elog(ERROR, "unrecognized encryption algorithm: %s", val);
+				}
+				else
+					elog(ERROR, "unrecognized column encryption parameter: %s", el->defname);
+			}
+
+			if (!cek)
+				elog(ERROR, "column encryption key must be specified");
+
+			cekoid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid, PointerGetDatum(cek));
+			if (!cekoid)
+				elog(ERROR, "column encryption key \"%s\" does not exist", cek);
+
+			attr->attcek = cekoid;
+			attr->attrealtypid = attr->atttypid;
+			if (encdet)
+				attr->atttypid = ENCRYPTEDDOID;
+			else
+				attr->atttypid = ENCRYPTEDROID;
+			attr->attencalg = alg;
+		}
 	}
 
 	/*
@@ -6758,6 +6817,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attgenerated = colDef->generated;
 	attribute.attisdropped = false;
 	attribute.attislocal = colDef->is_local;
+	attribute.attcek = 0; // TODO
+	attribute.attrealtypid = 0; // TODO
+	attribute.attencalg = 0; // TODO
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
 
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 0568ae123f..b1bfe12eab 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2186,6 +2186,8 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan)
 						   NULL,
 						   plan->argtypes,
 						   plan->nargs,
+						   NULL,
+						   NULL,
 						   plan->parserSetup,
 						   plan->parserSetupArg,
 						   plan->cursor_options,
@@ -2423,6 +2425,8 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 							   NULL,
 							   plan->argtypes,
 							   plan->nargs,
+							   NULL,
+							   NULL,
 							   plan->parserSetup,
 							   plan->parserSetupArg,
 							   plan->cursor_options,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 297b6ee715..4f6255baa9 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3035,6 +3035,7 @@ _copyColumnDef(const ColumnDef *from)
 	COPY_STRING_FIELD(colname);
 	COPY_NODE_FIELD(typeName);
 	COPY_STRING_FIELD(compression);
+	COPY_NODE_FIELD(encryption);
 	COPY_SCALAR_FIELD(inhcount);
 	COPY_SCALAR_FIELD(is_local);
 	COPY_SCALAR_FIELD(is_not_null);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f537d3eb96..cf0a23a2c4 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2679,6 +2679,7 @@ _equalColumnDef(const ColumnDef *a, const ColumnDef *b)
 	COMPARE_STRING_FIELD(colname);
 	COMPARE_NODE_FIELD(typeName);
 	COMPARE_STRING_FIELD(compression);
+	COMPARE_NODE_FIELD(encryption);
 	COMPARE_SCALAR_FIELD(inhcount);
 	COMPARE_SCALAR_FIELD(is_local);
 	COMPARE_SCALAR_FIELD(is_not_null);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index e276264882..33621141f1 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3917,6 +3917,8 @@ raw_expression_tree_walker(Node *node,
 					return true;
 				if (walker(coldef->compression, context))
 					return true;
+				if (walker(coldef->encryption, context))
+					return true;
 				if (walker(coldef->raw_default, context))
 					return true;
 				if (walker(coldef->collClause, context))
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 3600c9fa36..ecd2e86abf 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2953,6 +2953,7 @@ _outColumnDef(StringInfo str, const ColumnDef *node)
 	WRITE_STRING_FIELD(colname);
 	WRITE_NODE_FIELD(typeName);
 	WRITE_STRING_FIELD(compression);
+	WRITE_NODE_FIELD(encryption);
 	WRITE_INT_FIELD(inhcount);
 	WRITE_BOOL_FIELD(is_local);
 	WRITE_BOOL_FIELD(is_not_null);
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 146ee8dd1e..411c474a89 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -148,7 +148,8 @@ parse_analyze(RawStmt *parseTree, const char *sourceText,
  */
 Query *
 parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
-						Oid **paramTypes, int *numParams)
+						Oid **paramTypes, int *numParams,
+						Oid **paramOrigTbls, AttrNumber **paramOrigCols)
 {
 	ParseState *pstate = make_parsestate(NULL);
 	Query	   *query;
@@ -158,7 +159,7 @@ parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
 
 	pstate->p_sourcetext = sourceText;
 
-	parse_variable_parameters(pstate, paramTypes, numParams);
+	parse_variable_parameters(pstate, paramTypes, numParams, paramOrigTbls, paramOrigCols);
 
 	query = transformTopLevelStmt(pstate, parseTree);
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 86ce33bd97..449dc4e9b1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -568,6 +568,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
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_actions key_delete key_match key_update key_action
@@ -3476,12 +3477,13 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_compression opt_column_encryption create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 					n->colname = $1;
 					n->typeName = $2;
 					n->compression = $3;
+					n->encryption = $4;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3490,8 +3492,8 @@ columnDef:	ColId Typename opt_column_compression create_generic_options ColQualL
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $4;
-					SplitColQualList($5, &n->constraints, &n->collClause,
+					n->fdwoptions = $5;
+					SplitColQualList($6, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *)n;
@@ -3546,6 +3548,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 ColQualList:
 			ColQualList ColConstraint				{ $$ = lappend($1, $2); }
 			| /*EMPTY*/								{ $$ = NIL; }
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index 68a5534393..4fd5cb1443 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -49,6 +49,8 @@ typedef struct VarParamState
 {
 	Oid		  **paramTypes;		/* array of parameter type OIDs */
 	int		   *numParams;		/* number of array entries */
+	Oid		  **paramOrigTbls;
+	AttrNumber **paramOrigCols;
 } VarParamState;
 
 static Node *fixed_paramref_hook(ParseState *pstate, ParamRef *pref);
@@ -56,6 +58,7 @@ static Node *variable_paramref_hook(ParseState *pstate, ParamRef *pref);
 static Node *variable_coerce_param_hook(ParseState *pstate, Param *param,
 										Oid targetTypeId, int32 targetTypeMod,
 										int location);
+static void variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 static bool check_parameter_resolution_walker(Node *node, ParseState *pstate);
 static bool query_contains_extern_params_walker(Node *node, void *context);
 
@@ -81,15 +84,19 @@ parse_fixed_parameters(ParseState *pstate,
  */
 void
 parse_variable_parameters(ParseState *pstate,
-						  Oid **paramTypes, int *numParams)
+						  Oid **paramTypes, int *numParams,
+						  Oid **paramOrigTbls, AttrNumber **paramOrigCols)
 {
 	VarParamState *parstate = palloc(sizeof(VarParamState));
 
 	parstate->paramTypes = paramTypes;
 	parstate->numParams = numParams;
+	parstate->paramOrigTbls = paramOrigTbls;
+	parstate->paramOrigCols = paramOrigCols;
 	pstate->p_ref_hook_state = (void *) parstate;
 	pstate->p_paramref_hook = variable_paramref_hook;
 	pstate->p_coerce_param_hook = variable_coerce_param_hook;
+	pstate->p_param_assign_orig_hook = variable_param_assign_orig_hook;
 }
 
 /*
@@ -145,14 +152,36 @@ variable_paramref_hook(ParseState *pstate, ParamRef *pref)
 	{
 		/* Need to enlarge param array */
 		if (*parstate->paramTypes)
+		{
 			*parstate->paramTypes = (Oid *) repalloc(*parstate->paramTypes,
 													 paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc(*parstate->paramOrigTbls,
+													paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc(*parstate->paramOrigCols,
+													paramno * sizeof(AttrNumber));
+		}
 		else
+		{
 			*parstate->paramTypes = (Oid *) palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc(paramno * sizeof(AttrNumber));
+		}
 		/* Zero out the previously-unreferenced slots */
 		MemSet(*parstate->paramTypes + *parstate->numParams,
 			   0,
 			   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigTbls)
+			MemSet(*parstate->paramOrigTbls + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigCols)
+			MemSet(*parstate->paramOrigCols + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(AttrNumber));
 		*parstate->numParams = paramno;
 	}
 
@@ -260,6 +289,18 @@ variable_coerce_param_hook(ParseState *pstate, Param *param,
 	return NULL;
 }
 
+static void
+variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol)
+{
+	VarParamState *parstate = (VarParamState *) pstate->p_ref_hook_state;
+	int			paramno = param->paramid;
+
+	if (parstate->paramOrigTbls)
+		(*parstate->paramOrigTbls)[paramno - 1] = origtbl;
+	if (parstate->paramOrigCols)
+		(*parstate->paramOrigCols)[paramno - 1] = origcol;
+}
+
 /*
  * Check for consistent assignment of variable parameters after completion
  * of parsing with parse_variable_parameters.
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 9ce3a0de96..5d0093d7ac 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -595,6 +595,12 @@ transformAssignedExpr(ParseState *pstate,
 					 parser_errposition(pstate, exprLocation(orig_expr))));
 	}
 
+	if (IsA(expr, Param))
+	{
+		if (pstate->p_param_assign_orig_hook)
+			pstate->p_param_assign_orig_hook(pstate, castNode(Param, expr), RelationGetRelid(pstate->p_target_relation), attrno);
+	}
+
 	pstate->p_expr_kind = sv_expr_kind;
 
 	return expr;
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 82de01cdc6..da7ab9d68b 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -80,6 +80,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"
 
@@ -1336,6 +1337,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	bool		is_named;
 	bool		save_log_statement_stats = log_statement_stats;
 	char		msec_str[32];
+	Oid		   *paramOrigTbls = palloc(numParams * sizeof(Oid));
+	AttrNumber *paramOrigCols = palloc(numParams * sizeof(AttrNumber));
 
 	/*
 	 * Report query to various monitoring facilities.
@@ -1459,7 +1462,9 @@ exec_parse_message(const char *query_string,	/* string to execute */
 		query = parse_analyze_varparams(raw_parse_tree,
 										query_string,
 										&paramTypes,
-										&numParams);
+										&numParams,
+										&paramOrigTbls,
+										&paramOrigCols);
 
 		/*
 		 * Check all parameter types got determined.
@@ -1508,6 +1513,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 					   unnamed_stmt_context,
 					   paramTypes,
 					   numParams,
+					   paramOrigTbls,
+					   paramOrigCols,
 					   NULL,
 					   NULL,
 					   CURSOR_OPT_PARALLEL_OK,	/* allow parallel mode */
@@ -1805,6 +1812,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter corresponds to an encrypted column, but the parameter value was not encrypted")));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2602,8 +2619,47 @@ exec_describe_statement_message(const char *stmt_name)
 	{
 		Oid			ptype = psrc->param_types[i];
 
+		if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+		{
+			Oid			porigtbl = psrc->param_origtbls[i];
+			AttrNumber	porigcol = psrc->param_origcols[i];
+			HeapTuple   tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol <= 0)
+				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)));
+
+			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);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			ptype = orig_att->attrealtypid;
+
+			ReleaseSysCache(tp);
+		}
+
 		pq_sendint32(&row_description_buf, (int) ptype);
 	}
+
+	for (int i = 0; i < psrc->num_params; i++)
+	{
+		int16		pformat;
+
+		if (get_typtype(psrc->param_types[i]) == TYPTYPE_ENCRYPTED)
+		{
+			pformat = 0x10;
+			if (psrc->param_types[i] == ENCRYPTEDDOID)
+				pformat |= 0x20;
+		}
+		else
+			pformat = 0;
+
+		pq_sendint16(&row_description_buf, pformat);
+	}
+
 	pq_endmessage_reuse(&row_description_buf);
 
 	/*
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 6767eae8f2..869f37b26e 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -340,6 +340,8 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 				   MemoryContext querytree_context,
 				   Oid *param_types,
 				   int num_params,
+				   Oid *param_origtbls,
+				   AttrNumber *param_origcols,
 				   ParserSetupHook parserSetup,
 				   void *parserSetupArg,
 				   int cursor_options,
@@ -419,6 +421,14 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 	{
 		plansource->param_types = (Oid *) palloc(num_params * sizeof(Oid));
 		memcpy(plansource->param_types, param_types, num_params * sizeof(Oid));
+
+		plansource->param_origtbls = (Oid *) palloc0(num_params * sizeof(Oid));
+		if (param_origtbls)
+			memcpy(plansource->param_origtbls, param_origtbls, num_params * sizeof(Oid));
+
+		plansource->param_origcols = (AttrNumber *) palloc0(num_params * sizeof(AttrNumber));
+		if (param_origcols)
+			memcpy(plansource->param_origcols, param_origcols, num_params * sizeof(AttrNumber));
 	}
 	else
 		plansource->param_types = NULL;
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 56870b46e4..5c3f9b0055 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -29,7 +29,9 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.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"
@@ -266,6 +268,24 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		256
 	},
+	{
+		ColumnEncKeyRelationId,	/* CEKNAME */
+		ColumnEncKeyNameIndexId,
+		1,
+		{
+			Anum_pg_colenckey_cekname,
+		},
+		8
+	},
+	{
+		ColumnEncKeyRelationId,	/* CEKOID */
+		ColumnEncKeyOidIndexId,
+		1,
+		{
+			Anum_pg_colenckey_oid,
+		},
+		8
+	},
 	{OperatorClassRelationId,	/* CLAAMNAMENSP */
 		OpclassAmNameNspIndexId,
 		3,
@@ -288,6 +308,15 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
+	{
+		ColumnMasterKeyRelationId,	/* CMKOID */
+		ColumnMasterKeyOidIndexId,
+		1,
+		{
+			Anum_pg_colmasterkey_oid,
+		},
+		8
+	},
 	{CollationRelationId,		/* COLLNAMEENCNSP */
 		CollationNameEncNspIndexId,
 		3,
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index c9b37538a0..90d8c93564 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/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 8135854163..815cdac0af 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' },
 
+# encryptedd_ops
+{ amopfamily => 'hash/encryptedd_ops', amoplefttype => 'encryptedd',
+  amoprighttype => 'encryptedd', amopstrategy => '1', amopopr => '=(encryptedd,encryptedd)',
+  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 5460aa2422..a3698eea0f 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/encryptedd_ops', amproclefttype => 'encryptedd',
+  amprocrighttype => 'encryptedd', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/encryptedd_ops', amproclefttype => 'encryptedd',
+  amprocrighttype => 'encryptedd', 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 5c1ec9313e..48c774c053 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,15 @@ 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);
+
+	/* real type if encrypted */
+	Oid			attrealtypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+
+	/* encryption algorithm */
+	int32		attencalg BKI_DEFAULT(0);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index 67f73cb6fb..22ff9d5c0b 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -548,4 +548,10 @@
 { castsource => 'tstzrange', casttarget => 'tstzmultirange',
   castfunc => 'tstzmultirange(tstzrange)', castcontext => 'e',
   castmethod => 'f' },
+
+{ castsource => 'bytea', casttarget => 'encryptedd', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+{ castsource => 'bytea', casttarget => 'encryptedr', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+
 ]
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..a59abc3a16
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,18 @@
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_index, 8242, ColumnEncKeyNameIndexId, on pg_colenckey using btree(cekname name_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..cd19b0501b
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,25 @@
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+#ifdef CATALOG_VARLEN           /* variable-length fields start here */
+	text		ckdcmkalg BKI_FORCE_NOT_NULL;
+	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..15292b0b61
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,24 @@
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+#ifdef CATALOG_VARLEN           /* variable-length fields start here */
+	text		cmkprovider BKI_FORCE_NOT_NULL;
+	text		cmkoptions[1] BKI_FORCE_NOT_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_index, 8241, ColumnMasterKeyNameIndexId, on pg_colmasterkey using btree(cmkname name_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 484727a2fc..d41e2aac2a 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 => 'encryptedd_ops', opcfamily => 'hash/encryptedd_ops',
+  opcintype => 'encryptedd' },
 { 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 0075a02f32..9fb9890522 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3475,4 +3475,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'encryptedd',
+  oprright => 'encryptedd', oprresult => 'bool', oprcom => '=(encryptedd,encryptedd)',
+  oprnegate => '<>(encryptedd,encryptedd)', oprcode => 'encrypteddeq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'encryptedd', oprright => 'encryptedd', oprresult => 'bool',
+  oprcom => '<>(encryptedd,encryptedd)', oprnegate => '=(encryptedd,encryptedd)',
+  oprcode => 'encrypteddne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 8e480efd28..dcf9839e42 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 => 'encryptedd_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 79d787cd26..44a822aca5 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7992,9 +7992,9 @@
   proname => 'pg_prepared_statement', prorows => '1000', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'record',
   proargtypes => '',
-  proallargtypes => '{text,text,timestamptz,_regtype,bool,int8,int8}',
-  proargmodes => '{o,o,o,o,o,o,o}',
-  proargnames => '{name,statement,prepare_time,parameter_types,from_sql,generic_plans,custom_plans}',
+  proallargtypes => '{text,text,timestamptz,_regtype,bool,int8,int8,_regclass,_int2}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o}',
+  proargnames => '{name,statement,prepare_time,parameter_types,from_sql,generic_plans,custom_plans,parameter_orig_tables,parameter_orig_columns}',
   prosrc => 'pg_prepared_statement' },
 { oid => '2511', descr => 'get the open cursors for this session',
   proname => 'pg_cursor', prorows => '1000', proretset => 't',
@@ -11733,4 +11733,11 @@
   prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary',
   prosrc => 'brin_minmax_multi_summary_send' },
 
+{ oid => '8245',
+  proname => 'encrypteddeq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'encryptedd encryptedd', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'encrypteddne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'encryptedd encryptedd', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 41074c994b..36e2e2c263 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 => 'encryptedd', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'x' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'encryptedr', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'x' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index e568e21dee..999720fb54 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -277,6 +277,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPTYPE_MULTIRANGE	'm' /* multirange type */
 #define  TYPTYPE_PSEUDO		'p' /* pseudo-type */
 #define  TYPTYPE_RANGE		'r' /* range type */
+#define  TYPTYPE_ENCRYPTED	'y'	/* encrypted column value */
 
 #define  TYPCATEGORY_INVALID	'\0'	/* not an allowed category */
 #define  TYPCATEGORY_ARRAY		'A'
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 067138e6b5..04e6e310ed 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -670,6 +670,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? */
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index a0f0bd38d7..c1b8f7617a 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -27,7 +27,8 @@ extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook;
 extern Query *parse_analyze(RawStmt *parseTree, const char *sourceText,
 							Oid *paramTypes, int numParams, QueryEnvironment *queryEnv);
 extern Query *parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
-									  Oid **paramTypes, int *numParams);
+									  Oid **paramTypes, int *numParams,
+									  Oid **paramOrigTbls, AttrNumber **paramOrigCols);
 
 extern Query *parse_sub_analyze(Node *parseTree, ParseState *parentParseState,
 								CommonTableExpr *parentCTE,
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index ee179082ce..011162a63f 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -92,6 +92,7 @@ typedef Node *(*ParseParamRefHook) (ParseState *pstate, ParamRef *pref);
 typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param,
 								  Oid targetTypeId, int32 targetTypeMod,
 								  int location);
+typedef void (*ParamAssignOrigHook) (ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 
 
 /*
@@ -221,6 +222,7 @@ struct ParseState
 	PostParseColumnRefHook p_post_columnref_hook;
 	ParseParamRefHook p_paramref_hook;
 	CoerceParamHook p_coerce_param_hook;
+	ParamAssignOrigHook p_param_assign_orig_hook;
 	void	   *p_ref_hook_state;	/* common passthrough link for above */
 };
 
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index b42fff296c..b90a315e9c 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -18,7 +18,8 @@
 extern void parse_fixed_parameters(ParseState *pstate,
 								   Oid *paramTypes, int numParams);
 extern void parse_variable_parameters(ParseState *pstate,
-									  Oid **paramTypes, int *numParams);
+									  Oid **paramTypes, int *numParams,
+									  Oid **paramOrigTbls, AttrNumber **paramOrigCols);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index ff09c63a02..eac90aa821 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -101,6 +101,8 @@ typedef struct CachedPlanSource
 	CommandTag	commandTag;		/* 'nuff said */
 	Oid		   *param_types;	/* array of parameter type OIDs, or NULL */
 	int			num_params;		/* length of param_types array */
+	Oid		   *param_origtbls;
+	AttrNumber *param_origcols;
 	ParserSetupHook parserSetup;	/* alternative parameter spec method */
 	void	   *parserSetupArg;
 	int			cursor_options; /* cursor options used for planning */
@@ -199,6 +201,8 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 							   MemoryContext querytree_context,
 							   Oid *param_types,
 							   int num_params,
+							   Oid *param_origtbls,
+							   AttrNumber *param_origcols,
 							   ParserSetupHook parserSetup,
 							   void *parserSetupArg,
 							   int cursor_options,
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index c8cfbc30f6..2420c9a818 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,11 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKNAME,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..7dacc93e0b 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,5 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPrepared2           187
+PQsendQueryPrepared2      188
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 9b6a6939f0..bc1b492557 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -4151,6 +4151,11 @@ freePGconn(PGconn *conn)
 		free(conn->gsslib);
 	if (conn->connip)
 		free(conn->connip);
+	if (conn->cekdata)
+	{
+		explicit_bzero(conn->cekdata, conn->cekdatalen);
+		free(conn->cekdata);
+	}
 	/* Note that conn->Pfdebug is not ours to close or free */
 	if (conn->write_err_msg)
 		free(conn->write_err_msg);
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 6c7b3df012..0e9431f360 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -28,6 +28,9 @@
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
 
+/* TODO: move OpenSSL-specific parts to separate file */
+#include <openssl/evp.h>
+
 /* keep this in same order as ExecStatusType in libpq-fe.h */
 char	   *const pgresStatus[] = {
 	"PGRES_EMPTY_QUERY",
@@ -65,7 +68,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);
@@ -1089,6 +1093,113 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+void
+pqSaveColumnMasterKey(PGconn *conn, const char *name, const char *provider,
+								  const char *optname1, const char *optval1)
+{
+	free(conn->cmkname);
+	conn->cmkname = strdup(name);
+	free(conn->cmkprovider);
+	conn->cmkprovider = strdup(provider);
+	free(conn->cmkoptname1);
+	conn->cmkoptname1 = strdup(optname1);
+	free(conn->cmkoptval1);
+	conn->cmkoptval1 = strdup(optval1);
+}
+
+/*
+ * a different key provider would replace this function
+ */
+static unsigned char *
+decrypt_cek_file(PGconn *conn,
+				 int fromlen, const unsigned char *from,
+				 int *tolen)
+{
+	RSA *rsa;
+	char *cmkfilename = conn->cmkoptval1;
+	FILE *fp;
+	unsigned char *to;
+	int rv;
+
+	/*
+	 * setup, load keys
+	 */
+
+	if (!cmkfilename)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("no CMK file set\n"));
+		goto fail;
+	}
+	rsa = RSA_new();
+	fp = fopen(cmkfilename, "rb");
+	if (!fp)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("could not open file \"%s\": %m\n"), cmkfilename);
+		goto fail;
+	}
+	rsa = PEM_read_RSAPrivateKey(fp, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("PEM_read_RSAPrivateKey() failed: %s\n"),
+						  ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	fclose(fp);
+
+	/*
+	 * decrypt
+	 */
+
+	to = malloc(RSA_size(rsa));
+
+	rv = RSA_private_decrypt(fromlen, from, to, rsa, RSA_PKCS1_OAEP_PADDING);
+	if (rv < 0)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("RSA_private_decrypt() failed: %s\n"),
+						  ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	RSA_free(rsa);
+
+	*tolen = rv;
+	return to;
+
+fail:
+	return false;
+}
+
+static unsigned char *
+decrypt_cek(PGconn *conn,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	if (strcmp(conn->cmkprovider, "file") == 0)
+		return decrypt_cek_file(conn, fromlen, from, tolen);
+	else
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("CMK provider \"%s\" not recognized\n"), conn->cmkprovider);
+		return NULL;
+	}
+}
+
+void
+pqSaveColumnEncryptionKey(PGconn *conn, const char *name, const unsigned char *value, int len)
+{
+	unsigned char *newval = NULL;
+	int newvallen = 0;
+
+	newval = decrypt_cek(conn, len, value, &newvallen);
+
+	free(conn->cekdata);
+	conn->cekdata = newval;
+	conn->cekdatalen = newvallen;
+}
 
 /*
  * pqRowProcessor
@@ -1153,9 +1264,103 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 		}
 		else
 		{
-			bool		isbinary = (res->attDescs[i].format != 0);
+			bool		isbinary = ((res->attDescs[i].format & 0x0F) != 0);
+			bool		isencrypted = ((res->attDescs[i].format & 0xF0) != 0);
 			char	   *val;
 
+			if (isencrypted)
+			{
+				unsigned char *v;
+				size_t l;
+				unsigned char *iv;
+				size_t ivlen;
+
+				EVP_CIPHER_CTX *evp_ctx;
+				const EVP_CIPHER *cipher;
+				unsigned char *decr;
+				int decrlen, decrlen2;
+
+				cipher = EVP_get_cipherbyname("AES-128-CBC");
+				if (!cipher)
+				{
+					*errmsgp = libpq_gettext("EVP_get_cipherbyname() failed");
+					goto fail;
+				}
+
+				evp_ctx = EVP_CIPHER_CTX_new();
+
+				if (!isbinary)
+				{
+					unsigned char *x;
+					x = malloc(clen + 1);
+					memcpy(x, columns[i].value, clen);
+					x[clen] = '\0';
+					v = PQunescapeBytea(x, &l);
+				}
+				else
+				{
+					v = unconstify(unsigned char *, (const unsigned char *) columns[i].value);
+					l = clen;
+				}
+
+				if (!EVP_DecryptInit_ex(evp_ctx, cipher, NULL, NULL, NULL))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptInit_ex() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+
+				if (conn->cekdatalen != EVP_CIPHER_CTX_key_length(evp_ctx))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("column encryption key has wrong key length for cipher (has: %zu, required: %d)"),
+							 conn->cekdatalen, EVP_CIPHER_CTX_key_length(evp_ctx));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+
+				ivlen = EVP_CIPHER_CTX_iv_length(evp_ctx);
+				iv = v;
+				v += ivlen;
+				l -= ivlen;
+				if (!EVP_DecryptInit_ex(evp_ctx, NULL, NULL, conn->cekdata, iv))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptInit_ex() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+
+				decr = malloc(l + EVP_CIPHER_CTX_block_size(evp_ctx) + 1);
+				if (!EVP_DecryptUpdate(evp_ctx, decr, &decrlen, v, l))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptUpdate() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+				if (!EVP_DecryptFinal_ex(evp_ctx, decr + decrlen, &decrlen2))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptFinal_ex() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+				decrlen += decrlen2;
+				decr[decrlen] = '\0';
+				val = (char *) decr;
+				clen = decrlen + 1;
+
+				EVP_CIPHER_CTX_free(evp_ctx);
+				free(iv);
+			}
+			else
+			{
 			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
 			if (val == NULL)
 				goto fail;
@@ -1163,6 +1368,7 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			/* 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;
@@ -1470,7 +1676,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1588,6 +1795,19 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPrepared2(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+int
+PQsendQueryPrepared2(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;
@@ -1615,7 +1835,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1712,7 +1933,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1760,13 +1982,24 @@ 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;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->format & 0x10)
+					format |= 0x10;
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1785,6 +2018,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1803,8 +2037,93 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].format & 0x10)
+			{
+				bool enc_det = (paramDesc->paramDescs[i].format & 0x20) != 0;
+				unsigned char *v;
+				unsigned char *iv;
+				size_t ivlen;
+				EVP_CIPHER_CTX *evp_ctx;
+				const EVP_CIPHER *cipher;
+				unsigned char *encr;
+				int encrlen, encrlen2;
+				unsigned char *esc;
+				size_t esclen;
+
+				v = unconstify(unsigned char *, (const unsigned char *) paramValue);
+
+				cipher = EVP_get_cipherbyname("AES-128-CBC");
+				if (!cipher)
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_get_cipherbyname() failed\n"));
+					goto sendFailed;
+				}
+
+				evp_ctx = EVP_CIPHER_CTX_new();
+
+				if (!EVP_EncryptInit_ex(evp_ctx, cipher, NULL, NULL, NULL))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptInit_ex() failed: %s\n"),
+									  ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+
+				if (conn->cekdatalen != EVP_CIPHER_CTX_key_length(evp_ctx))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("column encryption key has wrong key length for cipher (has: %zu, required: %d)"),
+									  conn->cekdatalen, EVP_CIPHER_CTX_key_length(evp_ctx));
+					goto sendFailed;
+				}
+
+				ivlen = EVP_CIPHER_CTX_iv_length(evp_ctx);
+				iv = malloc(ivlen);
+				if (enc_det)
+					memset(iv, ivlen, 0);
+				else
+					pg_strong_random(iv, ivlen);
+				if (!EVP_EncryptInit_ex(evp_ctx, NULL, NULL, conn->cekdata, iv))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptInit_ex() failed: %s\n"),
+									  ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+
+				encr = malloc(ivlen + nbytes + EVP_CIPHER_CTX_block_size(evp_ctx) + 1);
+				memcpy(encr, iv, ivlen);
+				encr += ivlen;
+				if (!EVP_EncryptUpdate(evp_ctx, encr, &encrlen, v, nbytes))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptUpdate() failed: %s\n"),
+													ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+				if (!EVP_EncryptFinal_ex(evp_ctx, encr + encrlen, &encrlen2))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptFinal_ex() failed: %s\n"),
+									  ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+				encrlen += encrlen2;
+
+				EVP_CIPHER_CTX_free(evp_ctx);
+				free(iv);
+
+				esc = PQescapeByteaConn(conn, encr - ivlen, encrlen + ivlen, &esclen);
+				nbytes = esclen - 1;
+				paramValue = (char *) esc;
+			}
+
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
 		}
 		else
@@ -2258,12 +2577,25 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPrepared2(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+PGresult *
+PQexecPrepared2(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,
+	if (!PQsendQueryPrepared2(conn, stmtName,
 							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+							 paramFormats, resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 9ab3bf1fcb..c40550cdf6 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -45,6 +45,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);
@@ -315,6 +317,14 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					if (getColumnMasterKey(conn))
+						return;
+					break;
+				case 'Y':		/* Column Encryption Key */
+					if (getColumnEncryptionKey(conn))
+						return;
+					break;
 				case 'T':		/* Row Description */
 					if (conn->result != NULL &&
 						conn->result->resultStatus == PGRES_FATAL_ERROR)
@@ -606,7 +616,7 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		result->attDescs[i].typlen = typlen;
 		result->attDescs[i].atttypmod = atttypmod;
 
-		if (format != 1)
+		if ((format & 0x0F) != 1)
 			result->binary = 0;
 	}
 
@@ -713,6 +723,14 @@ getParamDescriptions(PGconn *conn, int msgLength)
 			goto not_enough_data;
 		result->paramDescs[i].typid = typid;
 	}
+	for (i = 0; i < nparams; i++)
+	{
+		int			format;
+
+		if (pqGetInt(&format, 2, conn))
+			goto not_enough_data;
+		result->paramDescs[i].format = format;
+	}
 
 	/* Success! */
 	conn->result = result;
@@ -1429,6 +1447,69 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	char *keyname;
+	char *keyprov;
+	int noptions;
+	char *optname = NULL;
+	char *optval = NULL;
+
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	/* Get the key provider */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyprov = strdup(conn->workBuffer.data);
+	/* Get the options */
+	if (pqGetInt(&noptions, 2, conn) != 0)
+		return EOF;
+	for (int i = 0; i < noptions; i++)
+	{
+		if (pqGets(&conn->workBuffer, conn) != 0)
+			return EOF;
+		optname = strdup(conn->workBuffer.data);
+		if (pqGets(&conn->workBuffer, conn) != 0)
+			return EOF;
+		optval = strdup(conn->workBuffer.data);
+	}
+	/* And save it */
+	pqSaveColumnMasterKey(conn, keyname, keyprov, optname, optval);
+
+	free(keyname);
+	free(keyprov);
+	free(optname);
+	free(optval);
+	return 0;
+}
+
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	char *buf;
+	int vallen;
+
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, 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 (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	pqSaveColumnEncryptionKey(conn, conn->workBuffer.data, (unsigned char *) buf, vallen);
+	free(buf);
+	return 0;
+}
 
 /*
  * Attempt to read a Notify response message.
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index 8660d27926..e99a5d15af 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -459,6 +459,8 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 
 	for (int i = 0; i < nfields; i++)
 		pqTraceOutputInt32(f, message, cursor, regress);
+	for (int i = 0; i < nfields; i++)
+		pqTraceOutputInt16(f, message, cursor);
 }
 
 /* RowDescription */
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index a6fd69aceb..a6c8ec974a 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -436,6 +436,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPrepared2(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
@@ -459,6 +467,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPrepared2(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);
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 490458adef..aa0f2eec2c 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -111,6 +111,7 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	int			format;			/* encrypted parameter? */
 } PGresParamDesc;
 
 /*
@@ -475,6 +476,14 @@ struct pg_conn
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
 
+	/* Encryption stuff */
+	char	   *cmkname;
+	char	   *cmkprovider;
+	char	   *cmkoptname1;	/* XXX only one option right now */
+	char	   *cmkoptval1;
+	unsigned char *cekdata;		/* column encryption key (decrypted) */
+	size_t		cekdatalen;
+
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
 	int			inBufSize;		/* allocated size of buffer */
@@ -648,6 +657,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern void pqSaveColumnMasterKey(PGconn *conn, const char *name, const char *provider,
+								  const char *optname1, const char *optval1);
+extern void pqSaveColumnEncryptionKey(PGconn *conn, const char *name,
+									  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/test/Makefile b/src/test/Makefile
index 46275915ff..e2c819b047 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -28,6 +28,7 @@ SUBDIRS += ldap
 endif
 endif
 ifeq ($(with_ssl),openssl)
+SUBDIRS += column_encryption
 ifneq (,$(filter ssl,$(PG_TEST_EXTRA)))
 SUBDIRS += ssl
 endif
@@ -37,7 +38,7 @@ endif
 # 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 ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos 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..1b9a5c72be
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,20 @@
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+override CPPFLAGS += -I$(libpq_srcdir)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+EXTRA_INSTALL = contrib/pgcrypto
+
+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/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..b4eb7323b5
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,135 @@
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 13;
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+$node->safe_psql('postgres', qq{CREATE EXTENSION pgcrypto});
+
+my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/cmk1.pem";
+
+# generate CMK
+system_or_bail 'openssl', 'genrsa', '-out', $cmkfilename;
+
+# generate 16 random bytes for 128-bit key
+system_or_bail 'openssl', 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin", 16;
+
+# encrypt CEK using CMK
+system_or_bail 'openssl', 'rsautl', '-encrypt', '-oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin",
+  '-inkey', $cmkfilename,
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin.enc";
+
+# XXX might as well capture stdout, not go through file
+my $cekbin = slurp_file "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin";
+my $cekhex = unpack('H*', $cekbin);
+my $cekencbin = slurp_file "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin.enc";
+my $cekenchex = unpack('H*', $cekencbin);
+
+# create CMK in database
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkprovider, cmkoptions) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    'cmk1', 'file', '{"filename", "$cmkfilename"}'::text[]
+);
+});
+
+# create CEK in database
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckey (oid, cekname) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    'cek1'
+);
+});
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    'RSA-OAEP',
+    '\\x${cekenchex}'
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+my $ivhex = '00' x 16;
+$node->safe_psql('postgres', qq{
+INSERT INTO tbl1 (a, b) VALUES (1, ('\\x${ivhex}'::bytea || encrypt_iv('val1', '\\x${cekhex}', '\\x${ivhex}', 'aes'))::encryptedr);
+INSERT INTO tbl1 (a, b) VALUES (2, ('\\x${ivhex}'::bytea || encrypt_iv('val2', '\\x${cekhex}', '\\x${ivhex}', 'aes'))::encryptedr);
+});
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'decrypted query result');
+
+
+$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 FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'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 FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3),
+	'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\t\\\\x[0-9a-z]{32}/,
+	'inserted data is encrypted');
+
+$node->command_ok(['test_client', 'test3'], 'test client test 3');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3upd),
+	'decrypted query result after test client insert 3');
+
+$node->command_ok(['test_client', 'test4'], 'test client test 4');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result after test client insert 4');
+
+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');
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..be37b51305
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,155 @@
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3"};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					   2, NULL, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res, *res2;
+	const char *values[] = {"3", "val3"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					2, 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;
+	}
+
+	res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res, *res2;
+	const char *values[] = {"3", "val3upd"};
+
+	res = PQprepare(conn, "", "UPDATE tbl1 SET b = $2 WHERE a = $1",
+					2, 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;
+	}
+
+	res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res, *res2;
+	const char *values[] = {"1", "valA", "2", "valB", "3", "valA"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6)",
+					6, 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;
+	}
+
+	res = PQexecPrepared2(conn, "", 6, values, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	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/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace
index 1a7de5c3e6..ce84f1955b 100644
--- a/src/test/modules/libpq_pipeline/traces/prepared.trace
+++ b/src/test/modules/libpq_pipeline/traces/prepared.trace
@@ -2,7 +2,7 @@ F	68	Parse	 "select_one" "SELECT $1, '42', $1::numeric, interval '1 sec'" 1 NNNN
 F	16	Describe	 S "select_one"
 F	4	Sync
 B	4	ParseComplete
-B	10	ParameterDescription	 1 NNNN
+B	12	ParameterDescription	 1 NNNN 0
 B	113	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 65535 -1 0 "numeric" NNNN 0 NNNN 65535 -1 0 "interval" NNNN 0 NNNN 16 -1 0
 B	5	ReadyForQuery	 I
 F	10	Query	 "BEGIN"
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..1fec9f8903
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,47 @@
+/* Imagine:
+CREATE COLUMN MASTER KEY cmk1 (
+    provider = 'test'
+);
+*/
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkprovider, cmkoptions) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    'cmk1', 'test', '{}'::text[]
+);
+/* Imagine:
+CREATE COLUMN ENCRYPTION KEY cek1 (
+    column_master_key = cmk1,
+    algorithm = '...',
+    encrypted_value = '...'
+);
+*/
+INSERT INTO pg_colenckey (oid, cekname) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    'cek1'
+);
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    '2ROT13',
+    '\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_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      | encryptedr | default   |          | 
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..ab6405a6c3 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 {attrealtypid} => 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,5 @@ 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_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 562b586d8e..fc2a07971d 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -175,13 +175,14 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | encryptedd
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+(7 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,11 +198,12 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | encryptedd
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(4 rows)
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -840,6 +842,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+encrypteddeq(encryptedd,encryptedd)
+encrypteddne(encryptedd,encryptedd)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
@@ -987,7 +991,9 @@ WHERE c.castmethod = 'b' AND
  xml               | text              |        0 | a
  xml               | character varying |        0 | a
  xml               | character         |        0 | a
-(10 rows)
+ bytea             | encryptedd        |        0 | e
+ bytea             | encryptedr        |        0 | e
+(12 rows)
 
 -- **************** pg_conversion ****************
 -- Look for illegal values in pg_conversion fields.
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 3306c696b1..675556c7d1 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -159,25 +159,30 @@ PREPARE q6 AS
     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;
 PREPARE q7(unknown) AS
     SELECT * FROM road WHERE thepath = $1;
-SELECT name, statement, parameter_types FROM pg_prepared_statements
+-- DML statements
+PREPARE q8 AS
+    UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;
+SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns FROM pg_prepared_statements
     ORDER BY name;
- name |                            statement                             |                  parameter_types                   
-------+------------------------------------------------------------------+----------------------------------------------------
- q2   | PREPARE q2(text) AS                                             +| {text}
-      |         SELECT datname, datistemplate, datallowconn             +| 
-      |         FROM pg_database WHERE datname = $1;                     | 
- q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint}
-      |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+| 
-      |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +| 
-      |         ORDER BY unique1;                                        | 
- q5   | PREPARE q5(int, text) AS                                        +| {integer,text}
-      |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +| 
-      |         ORDER BY unique1;                                        | 
- q6   | PREPARE q6 AS                                                   +| {integer,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    | 
- q7   | PREPARE q7(unknown) AS                                          +| {path}
-      |     SELECT * FROM road WHERE thepath = $1;                       | 
-(5 rows)
+ name |                            statement                             |                  parameter_types                   | parameter_orig_tables | parameter_orig_columns 
+------+------------------------------------------------------------------+----------------------------------------------------+-----------------------+------------------------
+ q2   | PREPARE q2(text) AS                                             +| {text}                                             | {-}                   | {0}
+      |         SELECT datname, datistemplate, datallowconn             +|                                                    |                       | 
+      |         FROM pg_database WHERE datname = $1;                     |                                                    |                       | 
+ q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {-,-,-,-,-}           | {0,0,0,0,0}
+      |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    |                       | 
+      |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    |                       | 
+      |         ORDER BY unique1;                                        |                                                    |                       | 
+ q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {-,-}                 | {0,0}
+      |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    |                       | 
+      |         ORDER BY unique1;                                        |                                                    |                       | 
+ q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {-,-}                 | {0,0}
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    |                       | 
+ q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {-}                   | {0}
+      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    |                       | 
+ q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | {-,tenk1}             | {0,14}
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    |                       | 
+(6 rows)
 
 -- test DEALLOCATE ALL;
 DEALLOCATE ALL;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index b58b062b10..7883baf4ab 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1441,8 +1441,10 @@ pg_prepared_statements| SELECT p.name,
     p.parameter_types,
     p.from_sql,
     p.generic_plans,
-    p.custom_plans
-   FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, from_sql, generic_plans, custom_plans);
+    p.custom_plans,
+    p.parameter_orig_tables,
+    p.parameter_orig_columns
+   FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, from_sql, generic_plans, custom_plans, parameter_orig_tables, parameter_orig_columns);
 pg_prepared_xacts| SELECT p.transaction,
     p.gid,
     p.prepared,
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 63706a28cc..67a0fcc9a3 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -112,7 +112,10 @@ pg_auth_members|t
 pg_authid|t
 pg_cast|t
 pg_class|t
+pg_colenckey|t
+pg_colenckeydata|t
 pg_collation|t
+pg_colmasterkey|t
 pg_constraint|t
 pg_conversion|t
 pg_database|t
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 257b6cac12..403e9f5c1e 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -17,7 +17,7 @@ SELECT p1.oid, p1.typname
 FROM pg_type as p1
 WHERE p1.typnamespace = 0 OR
     (p1.typlen <= 0 AND p1.typlen != -1 AND p1.typlen != -2) OR
-    (p1.typtype not in ('b', 'c', 'd', 'e', 'p', 'r', 'm')) OR
+    (p1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT p1.typisdefined OR
     (p1.typalign not in ('c', 's', 'i', 'd')) OR
     (p1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -75,7 +75,9 @@ ORDER BY p1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | encryptedd
+ 8244 | encryptedr
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
@@ -210,7 +212,8 @@ ORDER BY 1;
  e       | enum_in
  m       | multirange_in
  r       | range_in
-(5 rows)
+ y       | byteain
+(6 rows)
 
 -- Check for bogus typoutput routines
 -- As of 8.0, this check finds refcursor, which is borrowing
@@ -255,7 +258,8 @@ ORDER BY 1;
  e       | enum_out
  m       | multirange_out
  r       | range_out
-(4 rows)
+ y       | byteaout
+(5 rows)
 
 -- Domains should have same typoutput as their base types
 SELECT p1.oid, p1.typname, p2.oid, p2.typname
@@ -335,7 +339,8 @@ ORDER BY 1;
  e       | enum_recv
  m       | multirange_recv
  r       | range_recv
-(5 rows)
+ y       | bytearecv
+(6 rows)
 
 -- Check for bogus typsend routines
 -- As of 7.4, this check finds refcursor, which is borrowing
@@ -380,7 +385,8 @@ ORDER BY 1;
  e       | enum_send
  m       | multirange_send
  r       | range_send
-(4 rows)
+ y       | byteasend
+(5 rows)
 
 -- Domains should have same typsend as their base types
 SELECT p1.oid, p1.typname, p2.oid, p2.typname
@@ -707,6 +713,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::encryptedr,
+  E'\\xDEADBEEF'::encryptedd,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 017e962fed..cfc7c4e69a 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,6 +123,9 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # ----------
 test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize
 
+# WIP
+test: column_encryption
+
 # event triggers cannot run concurrently with any test that runs DDL
 # oidjoins is read-only, though, and should run late for best coverage
 test: event_trigger oidjoins
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..745d6c2ede
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,44 @@
+/* Imagine:
+CREATE COLUMN MASTER KEY cmk1 (
+    provider = 'test'
+);
+*/
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkprovider, cmkoptions) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    'cmk1', 'test', '{}'::text[]
+);
+
+/* Imagine:
+CREATE COLUMN ENCRYPTION KEY cek1 (
+    column_master_key = cmk1,
+    algorithm = '...',
+    encrypted_value = '...'
+);
+*/
+INSERT INTO pg_colenckey (oid, cekname) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    'cek1'
+);
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    '2ROT13',
+    '\xDEADBEEF'
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+\d tbl_29f3
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index 985d0f05c9..b2aa96d370 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -71,7 +71,11 @@ CREATE TEMPORARY TABLE q5_prep_nodata AS EXECUTE q5(200, 'DTAAAA')
 PREPARE q7(unknown) AS
     SELECT * FROM road WHERE thepath = $1;
 
-SELECT name, statement, parameter_types FROM pg_prepared_statements
+-- DML statements
+PREPARE q8 AS
+    UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;
+
+SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns FROM pg_prepared_statements
     ORDER BY name;
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 8281076423..f31f9da139 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -20,7 +20,7 @@
 FROM pg_type as p1
 WHERE p1.typnamespace = 0 OR
     (p1.typlen <= 0 AND p1.typlen != -1 AND p1.typlen != -2) OR
-    (p1.typtype not in ('b', 'c', 'd', 'e', 'p', 'r', 'm')) OR
+    (p1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT p1.typisdefined OR
     (p1.typalign not in ('c', 's', 'i', 'd')) OR
     (p1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -529,6 +529,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::encryptedr,
+  E'\\xDEADBEEF'::encryptedd,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,

base-commit: 49422ad0cc88c91a38522b2a7b222c2f2c939f82
-- 
2.34.1



view thread (53+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected]
  Subject: Re: Transparent column encryption
  In-Reply-To: <[email protected]>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox