From 9a64d9b1bcb0d9cda45ec716f579d2cc44122f92 Mon Sep 17 00:00:00 2001
From: Nitin <nitin.jadhav@enterprisedb.com>
Date: Wed, 25 Aug 2021 17:57:27 +0530
Subject: [PATCH] Multi Column List Partitioning

Supported list partitioning based on multiple columns.
Supported new syantx to allow mentioning multiple key information.
Created a infrastructure to accomodate multiple NULL values in
case of list partitioning. Supported partition pruning mechanism
to work for multiple keys.
---
 src/backend/commands/tablecmds.c              |   7 -
 src/backend/executor/execPartition.c          |  10 +-
 src/backend/parser/parse_utilcmd.c            | 198 +++++---
 src/backend/partitioning/partbounds.c         | 625 ++++++++++++++++++++------
 src/backend/partitioning/partprune.c          | 439 +++++++++++++-----
 src/backend/utils/adt/ruleutils.c             |  45 +-
 src/include/partitioning/partbounds.h         |  22 +-
 src/include/utils/ruleutils.h                 |   1 +
 src/test/regress/expected/create_table.out    |  34 +-
 src/test/regress/expected/insert.out          | 147 ++++++
 src/test/regress/expected/partition_prune.out | 432 ++++++++++++++++++
 src/test/regress/sql/create_table.sql         |  25 +-
 src/test/regress/sql/insert.sql               |  64 +++
 src/test/regress/sql/partition_prune.sql      |  42 ++
 14 files changed, 1752 insertions(+), 339 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a030771..ac0812c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16669,13 +16669,6 @@ transformPartitionSpec(Relation rel, PartitionSpec *partspec, char *strategy)
 				 errmsg("unrecognized partitioning strategy \"%s\"",
 						partspec->strategy)));
 
-	/* Check valid number of columns for strategy */
-	if (*strategy == PARTITION_STRATEGY_LIST &&
-		list_length(partspec->partParams) != 1)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-				 errmsg("cannot use \"list\" partition strategy with more than one column")));
-
 	/*
 	 * Create a dummy ParseState and insert the target relation as its sole
 	 * rangetable entry.  We need a ParseState for transformExpr.
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 5c723bc..f7b965a 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -1265,19 +1265,13 @@ get_partition_for_tuple(PartitionDispatch pd, Datum *values, bool *isnull)
 			break;
 
 		case PARTITION_STRATEGY_LIST:
-			if (isnull[0])
-			{
-				if (partition_bound_accepts_nulls(boundinfo))
-					part_index = boundinfo->null_index;
-			}
-			else
 			{
 				bool		equal = false;
 
 				bound_offset = partition_list_bsearch(key->partsupfunc,
 													  key->partcollation,
-													  boundinfo,
-													  values[0], &equal);
+													  boundinfo, values, isnull,
+													  key->partnatts, &equal);
 				if (bound_offset >= 0 && equal)
 					part_index = boundinfo->indexes[bound_offset];
 			}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 675e400..756f3ec 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -142,6 +142,9 @@ static void validateInfiniteBounds(ParseState *pstate, List *blist);
 static Const *transformPartitionBoundValue(ParseState *pstate, Node *con,
 										   const char *colName, Oid colType, int32 colTypmod,
 										   Oid partCollation);
+static List *transformPartitionListBounds(ParseState *pstate,
+										  PartitionBoundSpec *spec,
+										  Relation parent);
 
 
 /*
@@ -3984,6 +3987,42 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd)
 }
 
 /*
+ * checkForDuplicates
+ *
+ * Returns TRUE if the list bound element is already present in the list of
+ * list bounds, FALSE otherwise.
+ */
+static bool
+checkForDuplicates(List *source, List *searchElem)
+{
+	ListCell   *cell = NULL;
+
+	foreach(cell, source)
+	{
+		int		i = 0;
+		List   *elem = lfirst(cell);
+		bool	isDuplicate	= true;
+
+		for (i = 0; i < list_length(elem); i++)
+		{
+			Const   *value1 = castNode(Const, list_nth(elem, i));
+			Const   *value2 = castNode(Const, list_nth(searchElem, i));
+
+			if (!equal(value1, value2))
+			{
+				isDuplicate = false;
+				break;
+			}
+		}
+
+		if (isDuplicate)
+			return true;
+	}
+
+	return false;
+}
+
+/*
  * transformPartitionBound
  *
  * Transform a partition bound specification
@@ -3996,7 +4035,6 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 	PartitionKey key = RelationGetPartitionKey(parent);
 	char		strategy = get_partition_strategy(key);
 	int			partnatts = get_partition_natts(key);
-	List	   *partexprs = get_partition_exprs(key);
 
 	/* Avoid scribbling on input */
 	result_spec = copyObject(spec);
@@ -4046,62 +4084,14 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 	}
 	else if (strategy == PARTITION_STRATEGY_LIST)
 	{
-		ListCell   *cell;
-		char	   *colname;
-		Oid			coltype;
-		int32		coltypmod;
-		Oid			partcollation;
-
 		if (spec->strategy != PARTITION_STRATEGY_LIST)
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
 					 errmsg("invalid bound specification for a list partition"),
 					 parser_errposition(pstate, exprLocation((Node *) spec))));
 
-		/* Get the only column's name in case we need to output an error */
-		if (key->partattrs[0] != 0)
-			colname = get_attname(RelationGetRelid(parent),
-								  key->partattrs[0], false);
-		else
-			colname = deparse_expression((Node *) linitial(partexprs),
-										 deparse_context_for(RelationGetRelationName(parent),
-															 RelationGetRelid(parent)),
-										 false, false);
-		/* Need its type data too */
-		coltype = get_partition_col_typid(key, 0);
-		coltypmod = get_partition_col_typmod(key, 0);
-		partcollation = get_partition_col_collation(key, 0);
-
-		result_spec->listdatums = NIL;
-		foreach(cell, spec->listdatums)
-		{
-			Node	   *expr = lfirst(cell);
-			Const	   *value;
-			ListCell   *cell2;
-			bool		duplicate;
-
-			value = transformPartitionBoundValue(pstate, expr,
-												 colname, coltype, coltypmod,
-												 partcollation);
-
-			/* Don't add to the result if the value is a duplicate */
-			duplicate = false;
-			foreach(cell2, result_spec->listdatums)
-			{
-				Const	   *value2 = lfirst_node(Const, cell2);
-
-				if (equal(value, value2))
-				{
-					duplicate = true;
-					break;
-				}
-			}
-			if (duplicate)
-				continue;
-
-			result_spec->listdatums = lappend(result_spec->listdatums,
-											  value);
-		}
+		result_spec->listdatums =
+			transformPartitionListBounds(pstate, spec, parent);
 	}
 	else if (strategy == PARTITION_STRATEGY_RANGE)
 	{
@@ -4138,6 +4128,112 @@ transformPartitionBound(ParseState *pstate, Relation parent,
 }
 
 /*
+ * transformPartitionListBounds
+ *
+ * Converts the expressions of list partition bounds from the raw grammar
+ * representation.
+ */
+static List *
+transformPartitionListBounds(ParseState *pstate, PartitionBoundSpec *spec,
+							 Relation parent)
+{
+	int				i = 0;
+	int				j = 0;
+	ListCell	   *cell = NULL;
+	List		   *result = NIL;
+	PartitionKey	key = RelationGetPartitionKey(parent);
+	List		   *partexprs = get_partition_exprs(key);
+	int				partnatts = get_partition_natts(key);
+	char		  **colname = (char **) palloc0(partnatts * sizeof(char *));
+	Oid			   *coltype = palloc0(partnatts * sizeof(Oid));
+	int32		   *coltypmod = palloc0(partnatts * sizeof(int));
+	Oid			   *partcollation = palloc0(partnatts * sizeof(Oid));
+
+	for (i = 0; i < partnatts; i++)
+	{
+		if (key->partattrs[i] != 0)
+		{
+			colname[i] = (char *) palloc0(NAMEDATALEN * sizeof(char));
+			colname[i] = get_attname(RelationGetRelid(parent),
+									 key->partattrs[i], false);
+		}
+		else
+		{
+			colname[i] =
+				deparse_expression((Node *) list_nth(partexprs, j),
+								   deparse_context_for(RelationGetRelationName(parent),
+													   RelationGetRelid(parent)),
+								   false, false);
+			++j;
+		}
+
+		coltype[i] = get_partition_col_typid(key, i);
+		coltypmod[i] = get_partition_col_typmod(key, i);
+		partcollation[i] = get_partition_col_collation(key, i);
+	}
+
+	foreach(cell, spec->listdatums)
+	{
+		Node	   *expr = lfirst(cell);
+		List	   *values = NIL;
+		bool		isDuplicate = false;
+
+		if (partnatts == 1)
+		{
+			Const	   *val =
+				transformPartitionBoundValue(pstate, expr,colname[0],
+											 coltype[0], coltypmod[0],
+											 partcollation[0]);
+			values = lappend(values, val);
+		}
+		else
+		{
+			ListCell   *cell2 = NULL;
+			RowExpr		*rowexpr = NULL;
+
+			if (!IsA(expr, RowExpr))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("Invalid list bound specification"),
+						parser_errposition(pstate, exprLocation((Node *) spec))));
+
+			rowexpr = (RowExpr *) expr;
+			if (partnatts != list_length(rowexpr->args))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("Must specify exactly one value per partitioning column"),
+						 parser_errposition(pstate, exprLocation((Node *) spec))));
+
+			i = 0;
+			foreach(cell2, rowexpr->args)
+			{
+				Node       *expr = lfirst(cell2);
+				Const      *val =
+					transformPartitionBoundValue(pstate, expr, colname[i],
+												 coltype[i], coltypmod[i],
+												 partcollation[i]);
+				values = lappend(values, val);
+				i++;
+			}
+		}
+
+		/* Don't add to the result if the value is a duplicate */
+		isDuplicate = checkForDuplicates(result, values);
+		if (isDuplicate)
+			continue;
+
+		result = lappend(result, values);
+	}
+
+	pfree(colname);
+	pfree(coltype);
+	pfree(coltypmod);
+	pfree(partcollation);
+
+	return result;
+}
+
+/*
  * transformPartitionRangeBounds
  *		This converts the expressions for range partition bounds from the raw
  *		grammar representation to PartitionRangeDatum structs
diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index fdfe712..ca1e16b 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -53,11 +53,12 @@ typedef struct PartitionHashBound
 	int			index;
 } PartitionHashBound;
 
-/* One value coming from some (index'th) list partition */
+/* One bound of a list partition */
 typedef struct PartitionListValue
 {
 	int			index;
-	Datum		value;
+	Datum	   *values;
+	bool	   *isnulls;
 } PartitionListValue;
 
 /* One bound of a range partition */
@@ -102,7 +103,8 @@ static PartitionBoundInfo create_list_bounds(PartitionBoundSpec **boundspecs,
 											 int nparts, PartitionKey key, int **mapping);
 static PartitionBoundInfo create_range_bounds(PartitionBoundSpec **boundspecs,
 											  int nparts, PartitionKey key, int **mapping);
-static PartitionBoundInfo merge_list_bounds(FmgrInfo *partsupfunc,
+static PartitionBoundInfo merge_list_bounds(int partnatts,
+											FmgrInfo *partsupfunc,
 											Oid *collations,
 											RelOptInfo *outer_rel,
 											RelOptInfo *inner_rel,
@@ -175,6 +177,7 @@ static void generate_matching_part_pairs(RelOptInfo *outer_rel,
 										 List **inner_parts);
 static PartitionBoundInfo build_merged_partition_bounds(char strategy,
 														List *merged_datums,
+														List *merged_isnulls,
 														List *merged_kinds,
 														List *merged_indexes,
 														int null_index,
@@ -230,6 +233,7 @@ static Oid	get_partition_operator(PartitionKey key, int col,
 								   StrategyNumber strategy, bool *need_relabel);
 static List *get_qual_for_hash(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_list(Relation parent, PartitionBoundSpec *spec);
+static List *get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec);
 static List *get_qual_for_range(Relation parent, PartitionBoundSpec *spec,
 								bool for_default);
 static void get_range_key_properties(PartitionKey key, int keynum,
@@ -366,7 +370,7 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
 	/* No special hash partitions. */
-	boundinfo->null_index = -1;
+	boundinfo->isnulls = NULL;
 	boundinfo->default_index = -1;
 
 	hbounds = (PartitionHashBound *)
@@ -438,28 +442,72 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 }
 
 /*
- * get_non_null_list_datum_count
- * 		Counts the number of non-null Datums in each partition.
+ * partition_bound_accepts_nulls
+ *
+ * Returns TRUE if partition bound has NULL value, FALSE otherwise.
  */
-static int
-get_non_null_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)
+bool
+partition_bound_accepts_nulls(PartitionBoundInfo boundinfo)
 {
-	int			i;
-	int			count = 0;
+	int i = 0;
+	int j = 0;
 
-	for (i = 0; i < nparts; i++)
-	{
-		ListCell   *lc;
+	if (!boundinfo->isnulls)
+		return false;
 
-		foreach(lc, boundspecs[i]->listdatums)
+	for (i = 0; i < boundinfo->ndatums; i++)
+	{
+		//TODO: Handle for multi-column cases
+		for (j = 0; j < 1; j++)
 		{
-			Const	   *val = lfirst_node(Const, lc);
+			if (boundinfo->isnulls[i][j])
+				return true;
+		}
+	}
+
+	return false;
+}
+
+/*
+ * get_partition_bound_null_index
+ *
+ * Returns the partition index of the partition bound which accepts NULL.
+ */
+int
+get_partition_bound_null_index(PartitionBoundInfo boundinfo)
+{
+	int i = 0;
+	int j = 0;
+
+	if (!boundinfo->isnulls)
+		return -1;
 
-			if (!val->constisnull)
-				count++;
+	for (i = 0; i < boundinfo->ndatums; i++)
+	{
+		//TODO: Handle for multi-column cases
+		for (j = 0; j < 1; j++)
+		{
+			if (boundinfo->isnulls[i][j])
+				return boundinfo->indexes[i];
 		}
 	}
 
+	return -1;
+}
+
+/*
+ * get_list_datum_count
+ * 		Counts the number of Datums in each partition.
+ */
+static int
+get_list_datum_count(PartitionBoundSpec **boundspecs, int nparts)
+{
+	int			i;
+	int			count = 0;
+
+	for (i = 0; i < nparts; i++)
+		count += list_length(boundspecs[i]->listdatums);
+
 	return count;
 }
 
@@ -472,25 +520,23 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 				   PartitionKey key, int **mapping)
 {
 	PartitionBoundInfo boundinfo;
-	PartitionListValue *all_values;
+	PartitionListValue **all_values;
 	int			i;
 	int			j;
 	int			ndatums;
 	int			next_index = 0;
 	int			default_index = -1;
-	int			null_index = -1;
 	Datum	   *boundDatums;
 
 	boundinfo = (PartitionBoundInfoData *)
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
 	/* Will be set correctly below. */
-	boundinfo->null_index = -1;
 	boundinfo->default_index = -1;
 
-	ndatums = get_non_null_list_datum_count(boundspecs, nparts);
-	all_values = (PartitionListValue *)
-		palloc(ndatums * sizeof(PartitionListValue));
+	ndatums = get_list_datum_count(boundspecs, nparts);
+	all_values = (PartitionListValue **)
+		palloc(ndatums * sizeof(PartitionListValue *));
 
 	/* Create a unified list of non-null values across all partitions. */
 	for (j = 0, i = 0; i < nparts; i++)
@@ -514,35 +560,40 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 
 		foreach(c, spec->listdatums)
 		{
-			Const	   *val = lfirst_node(Const, c);
+			int 			k = 0;
+			List		   *elem = lfirst(c);
+			ListCell	   *cell = NULL;
 
-			if (!val->constisnull)
-			{
-				all_values[j].index = i;
-				all_values[j].value = val->constvalue;
-				j++;
-			}
-			else
+			all_values[j] = (PartitionListValue *) palloc(sizeof(PartitionListValue));
+			all_values[j]->values = (Datum *) palloc0(key->partnatts * sizeof(Datum));
+			all_values[j]->isnulls = (bool *) palloc0(key->partnatts * sizeof(bool));
+			all_values[j]->index = i;
+
+			foreach(cell, elem)
 			{
-				/*
-				 * Never put a null into the values array; save the index of
-				 * the partition that stores nulls, instead.
-				 */
-				if (null_index != -1)
-					elog(ERROR, "found null more than once");
-				null_index = i;
+				Const      *val = lfirst_node(Const, cell);
+
+				if (!val->constisnull)
+					all_values[j]->values[k] = val->constvalue;
+				else
+					all_values[j]->isnulls[k] = true;
+
+				k++;
 			}
+
+			j++;
 		}
 	}
 
 	/* ensure we found a Datum for every slot in the all_values array */
 	Assert(j == ndatums);
 
-	qsort_arg(all_values, ndatums, sizeof(PartitionListValue),
+	qsort_arg(all_values, ndatums, sizeof(PartitionListValue *),
 			  qsort_partition_list_value_cmp, (void *) key);
 
 	boundinfo->ndatums = ndatums;
 	boundinfo->datums = (Datum **) palloc0(ndatums * sizeof(Datum *));
+	boundinfo->isnulls = (bool **) palloc0(ndatums * sizeof(bool *));
 	boundinfo->kind = NULL;
 	boundinfo->interleaved_parts = NULL;
 	boundinfo->nindexes = ndatums;
@@ -563,12 +614,21 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	 */
 	for (i = 0; i < ndatums; i++)
 	{
-		int			orig_index = all_values[i].index;
+		int         j = 0;
+		int			orig_index = all_values[i]->index;
+		boundinfo->datums[i] = (Datum *) palloc(key->partnatts * sizeof(Datum));
+		boundinfo->isnulls[i] = (bool *) palloc(key->partnatts * sizeof(bool));
+
+
+		for (j = 0; j < key->partnatts; j++)
+		{
+			if (!all_values[i]->isnulls[j])
+				boundinfo->datums[i][j] = datumCopy(all_values[i]->values[j],
+													key->parttypbyval[j],
+													key->parttyplen[j]);
 
-		boundinfo->datums[i] = &boundDatums[i];
-		boundinfo->datums[i][0] = datumCopy(all_values[i].value,
-											key->parttypbyval[0],
-											key->parttyplen[0]);
+			boundinfo->isnulls[i][j] = all_values[i]->isnulls[j];
+		}
 
 		/* If the old index has no mapping, assign one */
 		if ((*mapping)[orig_index] == -1)
@@ -579,22 +639,6 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 
 	pfree(all_values);
 
-	/*
-	 * Set the canonical value for null_index, if any.
-	 *
-	 * It is possible that the null-accepting partition has not been assigned
-	 * an index yet, which could happen if such partition accepts only null
-	 * and hence not handled in the above loop which only looked at non-null
-	 * values.
-	 */
-	if (null_index != -1)
-	{
-		Assert(null_index >= 0);
-		if ((*mapping)[null_index] == -1)
-			(*mapping)[null_index] = next_index++;
-		boundinfo->null_index = (*mapping)[null_index];
-	}
-
 	/* Set the canonical value for default_index, if any. */
 	if (default_index != -1)
 	{
@@ -628,7 +672,6 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		 * expensive checks to look for interleaved values.
 		 */
 		if (boundinfo->ndatums +
-			partition_bound_accepts_nulls(boundinfo) +
 			partition_bound_has_default(boundinfo) != nparts)
 		{
 			int			last_index = -1;
@@ -646,16 +689,6 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 				if (index < last_index)
 					boundinfo->interleaved_parts = bms_add_member(boundinfo->interleaved_parts,
 																  index);
-
-				/*
-				 * Mark the NULL partition as interleaved if we find that it
-				 * allows some other non-NULL Datum.
-				 */
-				if (partition_bound_accepts_nulls(boundinfo) &&
-					index == boundinfo->null_index)
-					boundinfo->interleaved_parts = bms_add_member(boundinfo->interleaved_parts,
-																  boundinfo->null_index);
-
 				last_index = index;
 			}
 		}
@@ -701,8 +734,7 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	boundinfo = (PartitionBoundInfoData *)
 		palloc0(sizeof(PartitionBoundInfoData));
 	boundinfo->strategy = key->strategy;
-	/* There is no special null-accepting range partition. */
-	boundinfo->null_index = -1;
+	boundinfo->isnulls = NULL;
 	/* Will be set correctly below. */
 	boundinfo->default_index = -1;
 
@@ -905,6 +937,8 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 					   PartitionBoundInfo b1, PartitionBoundInfo b2)
 {
 	int			i;
+	bool		b1_isnull = false;
+	bool		b2_isnull = false;
 
 	if (b1->strategy != b2->strategy)
 		return false;
@@ -915,7 +949,7 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 	if (b1->nindexes != b2->nindexes)
 		return false;
 
-	if (b1->null_index != b2->null_index)
+	if (get_partition_bound_null_index(b1) != get_partition_bound_null_index(b2))
 		return false;
 
 	if (b1->default_index != b2->default_index)
@@ -988,7 +1022,22 @@ partition_bounds_equal(int partnatts, int16 *parttyplen, bool *parttypbyval,
 				 * context.  datumIsEqual() should be simple enough to be
 				 * safe.
 				 */
-				if (!datumIsEqual(b1->datums[i][j], b2->datums[i][j],
+				if (b1->isnulls)
+					b1_isnull = b1->isnulls[i][j];
+				if (b2->isnulls)
+					b2_isnull = b2->isnulls[i][j];
+
+				/*
+				 * If any of the partition bound has NULL value, then check
+				 * equality for the NULL value instead of comparing the datums
+				 * as it does not contain valid value in case of NULL.
+				 */
+				if (b1_isnull || b2_isnull)
+				{
+					if (b1_isnull != b2_isnull)
+						return false;
+				}
+				else if (!datumIsEqual(b1->datums[i][j], b2->datums[i][j],
 								  parttypbyval[j], parttyplen[j]))
 					return false;
 			}
@@ -1026,10 +1075,11 @@ partition_bounds_copy(PartitionBoundInfo src,
 	nindexes = dest->nindexes = src->nindexes;
 	partnatts = key->partnatts;
 
-	/* List partitioned tables have only a single partition key. */
-	Assert(key->strategy != PARTITION_STRATEGY_LIST || partnatts == 1);
-
 	dest->datums = (Datum **) palloc(sizeof(Datum *) * ndatums);
+	if (src->isnulls)
+		dest->isnulls = (bool **) palloc(sizeof(bool *) * ndatums);
+	else
+		dest->isnulls = NULL;
 
 	if (src->kind != NULL)
 	{
@@ -1075,6 +1125,8 @@ partition_bounds_copy(PartitionBoundInfo src,
 		int			j;
 
 		dest->datums[i] = &boundDatums[i * natts];
+		if (src->isnulls)
+			dest->isnulls[i] = (bool *) palloc(sizeof(bool) * natts);
 
 		for (j = 0; j < natts; j++)
 		{
@@ -1092,17 +1144,22 @@ partition_bounds_copy(PartitionBoundInfo src,
 				typlen = key->parttyplen[j];
 			}
 
-			if (dest->kind == NULL ||
-				dest->kind[i][j] == PARTITION_RANGE_DATUM_VALUE)
+			if ((dest->kind == NULL ||
+				 dest->kind[i][j] == PARTITION_RANGE_DATUM_VALUE) &&
+				(key->strategy != PARTITION_STRATEGY_LIST ||
+				 !src->isnulls[i][j]))
 				dest->datums[i][j] = datumCopy(src->datums[i][j],
 											   byval, typlen);
+
+			if (src->isnulls)
+				dest->isnulls[i][j] = src->isnulls[i][j];
+
 		}
 	}
 
 	dest->indexes = (int *) palloc(sizeof(int) * nindexes);
 	memcpy(dest->indexes, src->indexes, sizeof(int) * nindexes);
 
-	dest->null_index = src->null_index;
 	dest->default_index = src->default_index;
 
 	return dest;
@@ -1162,7 +1219,8 @@ partition_bounds_merge(int partnatts,
 			return NULL;
 
 		case PARTITION_STRATEGY_LIST:
-			return merge_list_bounds(partsupfunc,
+			return merge_list_bounds(partnatts,
+									 partsupfunc,
 									 partcollation,
 									 outer_rel,
 									 inner_rel,
@@ -1206,7 +1264,8 @@ partition_bounds_merge(int partnatts,
  * join can't handle.
  */
 static PartitionBoundInfo
-merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
+merge_list_bounds(int partnatts,
+				  FmgrInfo *partsupfunc, Oid *partcollation,
 				  RelOptInfo *outer_rel, RelOptInfo *inner_rel,
 				  JoinType jointype,
 				  List **outer_parts, List **inner_parts)
@@ -1220,6 +1279,8 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 	int			inner_default = inner_bi->default_index;
 	bool		outer_has_null = partition_bound_accepts_nulls(outer_bi);
 	bool		inner_has_null = partition_bound_accepts_nulls(inner_bi);
+	int			outer_null_index = get_partition_bound_null_index(outer_bi);
+	int			inner_null_index = get_partition_bound_null_index(inner_bi);
 	PartitionMap outer_map;
 	PartitionMap inner_map;
 	int			outer_pos;
@@ -1229,6 +1290,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 	int			default_index = -1;
 	List	   *merged_datums = NIL;
 	List	   *merged_indexes = NIL;
+	List	   *merged_isnulls = NIL;
 
 	Assert(*outer_parts == NIL);
 	Assert(*inner_parts == NIL);
@@ -1266,6 +1328,34 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		int			cmpval;
 		Datum	   *merged_datum = NULL;
 		int			merged_index = -1;
+		bool	   *outer_isnull;
+		bool	   *inner_isnull;
+		bool	   *merged_isnull = NULL;
+
+		if (outer_bi->isnulls && outer_pos < outer_bi->ndatums)
+			outer_isnull = outer_bi->isnulls[outer_pos];
+
+		if (inner_bi->isnulls && inner_pos < inner_bi->ndatums)
+			inner_isnull = inner_bi->isnulls[inner_pos];
+
+		//TODO: Handle for multi-column case.
+		if (outer_isnull[0] && inner_isnull[0])
+		{
+			outer_pos++;
+			inner_pos++;
+			continue;
+		}
+		else if (outer_isnull[0])
+		{
+			outer_pos++;
+			continue;
+		}
+		else if (inner_isnull[0])
+		{
+			inner_pos++;
+			continue;
+		}
+
 
 		if (outer_pos < outer_bi->ndatums)
 		{
@@ -1316,10 +1406,11 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		else
 		{
 			Assert(outer_datums != NULL && inner_datums != NULL);
-			cmpval = DatumGetInt32(FunctionCall2Coll(&partsupfunc[0],
-													 partcollation[0],
-													 outer_datums[0],
-													 inner_datums[0]));
+			//TODO: handle multi-column case
+			cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+												outer_datums, outer_isnull,
+												inner_datums, inner_isnull,
+												partnatts);
 		}
 
 		if (cmpval == 0)
@@ -1341,6 +1432,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				goto cleanup;
 
 			merged_datum = outer_datums;
+			merged_isnull = outer_isnull;
 
 			/* Move to the next pair of list values. */
 			outer_pos++;
@@ -1374,6 +1466,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = outer_datums;
+				merged_isnull = outer_isnull;
 			}
 
 			/* Move to the next list value on the outer side. */
@@ -1408,6 +1501,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 				if (merged_index == -1)
 					goto cleanup;
 				merged_datum = inner_datums;
+				merged_isnull = inner_isnull;
 			}
 
 			/* Move to the next list value on the inner side. */
@@ -1422,6 +1516,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		{
 			merged_datums = lappend(merged_datums, merged_datum);
 			merged_indexes = lappend_int(merged_indexes, merged_index);
+			merged_isnulls = lappend(merged_isnulls, merged_isnull);
 		}
 	}
 
@@ -1430,17 +1525,17 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 	 * non-existent.
 	 */
 	if (outer_has_null &&
-		is_dummy_partition(outer_rel, outer_bi->null_index))
+		is_dummy_partition(outer_rel, outer_null_index))
 		outer_has_null = false;
 	if (inner_has_null &&
-		is_dummy_partition(inner_rel, inner_bi->null_index))
+		is_dummy_partition(inner_rel, inner_null_index))
 		inner_has_null = false;
 
 	/* Merge the NULL partitions if any. */
 	if (outer_has_null || inner_has_null)
 		merge_null_partitions(&outer_map, &inner_map,
 							  outer_has_null, inner_has_null,
-							  outer_bi->null_index, inner_bi->null_index,
+							  outer_null_index, inner_null_index,
 							  jointype, &next_index, &null_index);
 	else
 		Assert(null_index == -1);
@@ -1478,6 +1573,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 		/* Make a PartitionBoundInfo struct to return. */
 		merged_bounds = build_merged_partition_bounds(outer_bi->strategy,
 													  merged_datums,
+													  merged_isnulls,
 													  NIL,
 													  merged_indexes,
 													  null_index,
@@ -1488,6 +1584,7 @@ merge_list_bounds(FmgrInfo *partsupfunc, Oid *partcollation,
 cleanup:
 	/* Free local memory before returning. */
 	list_free(merged_datums);
+	list_free(merged_isnulls);
 	list_free(merged_indexes);
 	free_partition_map(&outer_map);
 	free_partition_map(&inner_map);
@@ -1796,6 +1893,7 @@ merge_range_bounds(int partnatts, FmgrInfo *partsupfuncs,
 		/* Make a PartitionBoundInfo struct to return. */
 		merged_bounds = build_merged_partition_bounds(outer_bi->strategy,
 													  merged_datums,
+													  NIL,
 													  merged_kinds,
 													  merged_indexes,
 													  -1,
@@ -2527,19 +2625,41 @@ generate_matching_part_pairs(RelOptInfo *outer_rel, RelOptInfo *inner_rel,
  */
 static PartitionBoundInfo
 build_merged_partition_bounds(char strategy, List *merged_datums,
-							  List *merged_kinds, List *merged_indexes,
-							  int null_index, int default_index)
+							  List *merged_isnulls, List *merged_kinds,
+							  List *merged_indexes, int null_index,
+							  int default_index)
 {
 	PartitionBoundInfo merged_bounds;
 	int			ndatums = list_length(merged_datums);
 	int			pos;
 	ListCell   *lc;
+	int			natts = 1;  //TODO: Handle for multi-column case
+	bool	   *null = NULL;
 
 	merged_bounds = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData));
 	merged_bounds->strategy = strategy;
-	merged_bounds->ndatums = ndatums;
 
+	if (merged_isnulls)
+	{
+		if (null_index >= 0)
+		{
+			null = (bool *) palloc0(sizeof(bool) * natts);
+			null[0] = true;
+			ndatums++;
+		}
+		merged_bounds->isnulls = (bool **) palloc(sizeof(bool *) * ndatums);
+
+		pos = 0;
+		foreach(lc, merged_isnulls)
+			merged_bounds->isnulls[pos++] = (bool *) lfirst(lc);
+
+		if (null_index >= 0)
+			merged_bounds->isnulls[pos] = null;
+	}
+
+	merged_bounds->ndatums = ndatums;
 	merged_bounds->datums = (Datum **) palloc(sizeof(Datum *) * ndatums);
+
 	pos = 0;
 	foreach(lc, merged_datums)
 		merged_bounds->datums[pos++] = (Datum *) lfirst(lc);
@@ -2556,6 +2676,7 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 		/* There are ndatums+1 indexes in the case of range partitioning. */
 		merged_indexes = lappend_int(merged_indexes, -1);
 		ndatums++;
+		merged_bounds->isnulls = NULL;
 	}
 	else
 	{
@@ -2564,14 +2685,17 @@ build_merged_partition_bounds(char strategy, List *merged_datums,
 		merged_bounds->kind = NULL;
 	}
 
-	Assert(list_length(merged_indexes) == ndatums);
+	Assert(list_length(merged_indexes) == ndatums ||
+		   list_length(merged_indexes) == ndatums - 1);
 	merged_bounds->nindexes = ndatums;
 	merged_bounds->indexes = (int *) palloc(sizeof(int) * ndatums);
 	pos = 0;
 	foreach(lc, merged_indexes)
 		merged_bounds->indexes[pos++] = lfirst_int(lc);
 
-	merged_bounds->null_index = null_index;
+	if (merged_isnulls && null_index >= 0)
+		merged_bounds->indexes[pos] = null_index;
+
 	merged_bounds->default_index = default_index;
 
 	return merged_bounds;
@@ -3071,32 +3195,36 @@ check_new_partition_bound(char *relname, Relation parent,
 
 					foreach(cell, spec->listdatums)
 					{
-						Const	   *val = lfirst_node(Const, cell);
-
-						overlap_location = val->location;
-						if (!val->constisnull)
+						int			i = 0;
+						int         offset = -1;
+						bool        equal = false;
+						List	   *elem = lfirst(cell);
+						Datum	   *values = (Datum *) palloc0(key->partnatts * sizeof(Datum));
+						bool	   *isnulls = (bool *) palloc0(key->partnatts * sizeof(bool));
+
+						for (i = 0; i < key->partnatts; i++)
 						{
-							int			offset;
-							bool		equal;
-
-							offset = partition_list_bsearch(&key->partsupfunc[0],
-															key->partcollation,
-															boundinfo,
-															val->constvalue,
-															&equal);
-							if (offset >= 0 && equal)
-							{
-								overlap = true;
-								with = boundinfo->indexes[offset];
-								break;
-							}
+							Const	   *val = castNode(Const, list_nth(elem, i));
+
+							values[i] = val->constvalue;
+							isnulls[i] = val->constisnull;
+							overlap_location = val->location;
 						}
-						else if (partition_bound_accepts_nulls(boundinfo))
+
+						offset = partition_list_bsearch(key->partsupfunc,
+														key->partcollation,
+														boundinfo, values,
+														isnulls, key->partnatts,
+														&equal);
+						if (offset >= 0 && equal)
 						{
 							overlap = true;
-							with = boundinfo->null_index;
+							with = boundinfo->indexes[offset];
 							break;
 						}
+
+						pfree(values);
+						pfree(isnulls);
 					}
 				}
 
@@ -3609,6 +3737,48 @@ partition_hbound_cmp(int modulus1, int remainder1, int modulus2, int remainder2)
 }
 
 /*
+ * partition_lbound_datum_cmp
+ *
+ * Return whether list bound value (given by lb_datums and lb_isnulls) is
+ * <, =, or > partition key of a tuple (specified in values and isnulls).
+ *
+ * nvalues gives the number of values provided in the 'values' and 'isnulls'
+ * array.   partsupfunc and partcollation, both arrays of nvalues elements,
+ * give the comparison functions and the collations to be used when comparing.
+ */
+int32
+partition_lbound_datum_cmp(FmgrInfo *partsupfunc, Oid *partcollation,
+						   Datum *lb_datums, bool *lb_isnulls,
+						   Datum *values, bool *isnulls, int nvalues)
+{
+	int		i;
+	int32	cmpval;
+
+	for (i = 0; i < nvalues; i++)
+	{
+		/* This always places NULLs after not-NULLs. */
+		if (lb_isnulls[i])
+		{
+			if (isnulls && isnulls[i])
+				cmpval = 0;		/* NULL "=" NULL */
+			else
+				cmpval = 1;		/* NULL ">" not-NULL */
+		}
+		else if (isnulls && isnulls[i])
+			cmpval = -1;		/* not-NULL "<" NULL */
+		else
+			cmpval = DatumGetInt32(FunctionCall2Coll(&partsupfunc[i],
+													 partcollation[i],
+													 lb_datums[i], values[i]));
+
+		if (cmpval != 0)
+			break;
+	}
+
+	return cmpval;
+}
+
+/*
  * partition_list_bsearch
  *		Returns the index of the greatest bound datum that is less than equal
  * 		to the given value or -1 if all of the bound datums are greater
@@ -3618,8 +3788,8 @@ partition_hbound_cmp(int modulus1, int remainder1, int modulus2, int remainder2)
  */
 int
 partition_list_bsearch(FmgrInfo *partsupfunc, Oid *partcollation,
-					   PartitionBoundInfo boundinfo,
-					   Datum value, bool *is_equal)
+					   PartitionBoundInfo boundinfo, Datum *values,
+					   bool *isnulls, int nvalues, bool *is_equal)
 {
 	int			lo,
 				hi,
@@ -3632,10 +3802,10 @@ partition_list_bsearch(FmgrInfo *partsupfunc, Oid *partcollation,
 		int32		cmpval;
 
 		mid = (lo + hi + 1) / 2;
-		cmpval = DatumGetInt32(FunctionCall2Coll(&partsupfunc[0],
-												 partcollation[0],
-												 boundinfo->datums[mid][0],
-												 value));
+		cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+											boundinfo->datums[mid],
+											boundinfo->isnulls[mid],
+											values, isnulls, nvalues);
 		if (cmpval <= 0)
 		{
 			lo = mid;
@@ -3805,13 +3975,15 @@ qsort_partition_hbound_cmp(const void *a, const void *b)
 static int32
 qsort_partition_list_value_cmp(const void *a, const void *b, void *arg)
 {
-	Datum		val1 = ((PartitionListValue *const) a)->value,
-				val2 = ((PartitionListValue *const) b)->value;
+	Datum	   *vals1 = (*(PartitionListValue *const *) a)->values;
+	Datum	   *vals2 = (*(PartitionListValue *const *) b)->values;
+	bool	   *isnull1 = (*(PartitionListValue *const *) a)->isnulls;
+	bool	   *isnull2 = (*(PartitionListValue *const *) b)->isnulls;
 	PartitionKey key = (PartitionKey) arg;
 
-	return DatumGetInt32(FunctionCall2Coll(&key->partsupfunc[0],
-										   key->partcollation[0],
-										   val1, val2));
+	return partition_lbound_datum_cmp(key->partsupfunc, key->partcollation,
+									  vals1, isnull1, vals2, isnull2,
+									  key->partnatts);
 }
 
 /*
@@ -3911,9 +4083,8 @@ make_partition_op_expr(PartitionKey key, int keynum,
 				int			nelems = list_length(elems);
 
 				Assert(nelems >= 1);
-				Assert(keynum == 0);
 
-				if (nelems > 1 &&
+				if (key->partnatts == 1 && nelems > 1 &&
 					!type_is_array(key->parttypid[keynum]))
 				{
 					ArrayExpr  *arrexpr;
@@ -3942,7 +4113,7 @@ make_partition_op_expr(PartitionKey key, int keynum,
 
 					result = (Expr *) saopexpr;
 				}
-				else
+				else if (key->partnatts == 1)
 				{
 					List	   *elemops = NIL;
 					ListCell   *lc;
@@ -3963,6 +4134,15 @@ make_partition_op_expr(PartitionKey key, int keynum,
 
 					result = nelems > 1 ? makeBoolExpr(OR_EXPR, elemops, -1) : linitial(elemops);
 				}
+				else
+				{
+					result = make_opclause(operoid,
+										   BOOLOID,
+										   false,
+										   arg1, arg2,
+										   InvalidOid,
+										   key->partcollation[keynum]);
+				}
 				break;
 			}
 
@@ -4087,11 +4267,8 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 	List	   *elems = NIL;
 	bool		list_has_null = false;
 
-	/*
-	 * Only single-column list partitioning is supported, so we are worried
-	 * only about the partition key with index 0.
-	 */
-	Assert(key->partnatts == 1);
+	if (key->partnatts > 1)
+		return get_qual_for_multi_column_list(parent, spec);
 
 	/* Construct Var or expression representing the partition column */
 	if (key->partattrs[0] != 0)
@@ -4117,13 +4294,8 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 		PartitionBoundInfo boundinfo = pdesc->boundinfo;
 
 		if (boundinfo)
-		{
 			ndatums = boundinfo->ndatums;
 
-			if (partition_bound_accepts_nulls(boundinfo))
-				list_has_null = true;
-		}
-
 		/*
 		 * If default is the only partition, there need not be any partition
 		 * constraint on it.
@@ -4135,6 +4307,12 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 		{
 			Const	   *val;
 
+			if (boundinfo->isnulls[i][0])
+			{
+				list_has_null = true;
+				continue;
+			}
+
 			/*
 			 * Construct Const from known-not-null datum.  We must be careful
 			 * to copy the value, because our result has to be able to outlive
@@ -4144,7 +4322,7 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 							key->parttypmod[0],
 							key->parttypcoll[0],
 							key->parttyplen[0],
-							datumCopy(*boundinfo->datums[i],
+							datumCopy(boundinfo->datums[i][0],
 									  key->parttypbyval[0],
 									  key->parttyplen[0]),
 							false,	/* isnull */
@@ -4160,12 +4338,17 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 		 */
 		foreach(cell, spec->listdatums)
 		{
-			Const	   *val = lfirst_node(Const, cell);
+			ListCell	   *cell2 = NULL;
 
-			if (val->constisnull)
-				list_has_null = true;
-			else
-				elems = lappend(elems, copyObject(val));
+			foreach(cell2, (List *) lfirst(cell))
+			{
+				Const      *val = castNode(Const, lfirst(cell2));
+
+				if (val->constisnull)
+					list_has_null = true;
+				else
+					elems = lappend(elems, copyObject(val));
+			}
 		}
 	}
 
@@ -4241,6 +4424,158 @@ get_qual_for_list(Relation parent, PartitionBoundSpec *spec)
 }
 
 /*
+ * get_qual_for_list_for_multi_column
+ *
+ * Returns a list of expressions to use as a list partition's constraint,
+ * given the parent relation and partition bound structure.
+ *
+ * Returns NIL for a default partition when it's the only partition since
+ * in that case there is no constraint.
+ */
+static List *
+get_qual_for_multi_column_list(Relation parent, PartitionBoundSpec *spec)
+{
+	int			i = 0;
+	int			j = 0;
+	PartitionKey key = RelationGetPartitionKey(parent);
+	List	   *result;
+	Expr	   *opexpr;
+	NullTest   *nulltest;
+	ListCell   *cell;
+	List	   *elems = NIL;
+	Expr      **keyCol = (Expr **) palloc0 (key->partnatts * sizeof(Expr *));
+
+	/* Construct Var or expression representing the partition columns */
+	for (i = 0; i < key->partnatts; i++)
+	{
+		if (key->partattrs[i] != 0)
+			keyCol[i] = (Expr *) makeVar(1,
+									  key->partattrs[i],
+									  key->parttypid[i],
+									  key->parttypmod[i],
+									  key->parttypcoll[i],
+									  0);
+		else
+		{
+			keyCol[i] = (Expr *) copyObject(list_nth(key->partexprs, j));
+			++j;
+		}
+	}
+
+	/*
+	 * For default list partition, collect datums for all the partitions. The
+	 * default partition constraint should check that the partition key is
+	 * equal to none of those.
+	 */
+	if (spec->is_default)
+	{
+		int			ndatums = 0;
+		PartitionDesc pdesc = RelationGetPartitionDesc(parent, false);
+		PartitionBoundInfo boundinfo = pdesc->boundinfo;
+
+		if (boundinfo)
+			ndatums = boundinfo->ndatums;
+
+		/*
+		 * If default is the only partition, there need not be any partition
+		 * constraint on it.
+		 */
+		if (ndatums == 0)
+			return NIL;
+
+		for (i = 0; i < ndatums; i++)
+		{
+			List       *andexpr = NIL;
+
+			for (j = 0; j < key->partnatts; j++)
+			{
+				Const      *val = NULL;
+
+				if (boundinfo->isnulls[i][j])
+				{
+					nulltest = makeNode(NullTest);
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+					andexpr = lappend(andexpr, nulltest);
+				}
+				else
+				{
+					val = makeConst(key->parttypid[j],
+									key->parttypmod[j],
+									key->parttypcoll[j],
+									key->parttyplen[j],
+									datumCopy(boundinfo->datums[i][j],
+											  key->parttypbyval[j],
+											  key->parttyplen[j]),
+									false,  /* isnull */
+									key->parttypbyval[j]);
+
+					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
+													keyCol[j], (Expr *) val);
+					andexpr = lappend(andexpr, opexpr);
+				}
+			}
+
+			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
+			elems = lappend(elems, opexpr);
+		}
+	}
+	else
+	{
+		/*
+		 * Create list of Consts for the allowed values.
+		 */
+		foreach(cell, spec->listdatums)
+		{
+			List	   *andexpr = NIL;
+			ListCell   *cell2 = NULL;
+
+			j = 0;
+			foreach(cell2, (List *) lfirst(cell))
+			{
+				Const      *val = castNode(Const, lfirst(cell2));
+
+				if (val->constisnull)
+				{
+					nulltest = makeNode(NullTest);
+					nulltest->arg = keyCol[j];
+					nulltest->nulltesttype = IS_NULL;
+					nulltest->argisrow = false;
+					nulltest->location = -1;
+					andexpr = lappend(andexpr, nulltest);
+				}
+				else
+				{
+					opexpr = make_partition_op_expr(key, j, BTEqualStrategyNumber,
+													keyCol[j], (Expr *) val);
+					andexpr = lappend(andexpr, opexpr);
+				}
+				j++;
+			}
+
+			opexpr = makeBoolExpr(AND_EXPR, andexpr, -1);
+			elems = lappend(elems, opexpr);
+		}
+	}
+
+	opexpr = makeBoolExpr(OR_EXPR, elems, -1);
+	result = list_make1(opexpr);
+
+	/*
+	 * Note that, in general, applying NOT to a constraint expression doesn't
+	 * necessarily invert the set of rows it accepts, because NOT (NULL) is
+	 * NULL.  However, the partition constraints we construct here never
+	 * evaluate to NULL, so applying NOT works as intended.
+	 */
+	if (spec->is_default)
+		result = list_make1(makeBoolExpr(NOT_EXPR, result, -1));
+
+	return result;
+}
+
+/*
  * get_qual_for_range
  *
  * Returns an implicit-AND list of expressions to use as a range partition's
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index e00edbe..76ea26c 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -69,6 +69,8 @@ typedef struct PartClauseInfo
 	Oid			cmpfn;			/* Oid of function to compare 'expr' to the
 								 * partition key */
 	int			op_strategy;	/* btree strategy identifying the operator */
+	bool		is_null;		/* TRUE if clause contains NULL condition in case
+								   of list partitioning, FALSE otherwise */
 } PartClauseInfo;
 
 /*
@@ -185,8 +187,8 @@ static PruneStepResult *get_matching_hash_bounds(PartitionPruneContext *context,
 												 StrategyNumber opstrategy, Datum *values, int nvalues,
 												 FmgrInfo *partsupfunc, Bitmapset *nullkeys);
 static PruneStepResult *get_matching_list_bounds(PartitionPruneContext *context,
-												 StrategyNumber opstrategy, Datum value, int nvalues,
-												 FmgrInfo *partsupfunc, Bitmapset *nullkeys);
+												 StrategyNumber opstrategy, Datum *values, bool *isnulls,
+												 int nvalues, FmgrInfo *partsupfunc, Bitmapset *nullkeys);
 static PruneStepResult *get_matching_range_bounds(PartitionPruneContext *context,
 												  StrategyNumber opstrategy, Datum *values, int nvalues,
 												  FmgrInfo *partsupfunc, Bitmapset *nullkeys);
@@ -908,7 +910,8 @@ get_matching_partitions(PartitionPruneContext *context, List *pruning_steps)
 	{
 		Assert(context->strategy == PARTITION_STRATEGY_LIST);
 		Assert(partition_bound_accepts_nulls(context->boundinfo));
-		result = bms_add_member(result, context->boundinfo->null_index);
+		result = bms_add_member(result,
+								get_partition_bound_null_index(context->boundinfo));
 	}
 	if (scan_default)
 	{
@@ -1229,14 +1232,9 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 	 * Now generate some (more) pruning steps.  We have three strategies:
 	 *
 	 * 1) Generate pruning steps based on IS NULL clauses:
-	 *   a) For list partitioning, null partition keys can only be found in
-	 *      the designated null-accepting partition, so if there are IS NULL
-	 *      clauses containing partition keys we should generate a pruning
-	 *      step that gets rid of all partitions but that one.  We can
-	 *      disregard any OpExpr we may have found.
-	 *   b) For range partitioning, only the default partition can contain
+	 *   a) For range partitioning, only the default partition can contain
 	 *      NULL values, so the same rationale applies.
-	 *   c) For hash partitioning, we only apply this strategy if we have
+	 *   b) For hash partitioning, we only apply this strategy if we have
 	 *      IS NULL clauses for all the keys.  Strategy 2 below will take
 	 *      care of the case where some keys have OpExprs and others have
 	 *      IS NULL clauses.
@@ -1248,8 +1246,7 @@ gen_partprune_steps_internal(GeneratePruningStepsContext *context,
 	 *    IS NOT NULL clauses for all partition keys.
 	 */
 	if (!bms_is_empty(nullkeys) &&
-		(part_scheme->strategy == PARTITION_STRATEGY_LIST ||
-		 part_scheme->strategy == PARTITION_STRATEGY_RANGE ||
+		(part_scheme->strategy == PARTITION_STRATEGY_RANGE ||
 		 (part_scheme->strategy == PARTITION_STRATEGY_HASH &&
 		  bms_num_members(nullkeys) == part_scheme->partnatts)))
 	{
@@ -1399,10 +1396,12 @@ gen_prune_steps_from_opexps(GeneratePruningStepsContext *context,
 		bool		consider_next_key = true;
 
 		/*
-		 * For range partitioning, if we have no clauses for the current key,
-		 * we can't consider any later keys either, so we can stop here.
+		 * For range partitioning and list partitioning, if we have no clauses
+		 * for the current key, we can't consider any later keys either, so we
+		 * can stop here.
 		 */
-		if (part_scheme->strategy == PARTITION_STRATEGY_RANGE &&
+		if ((part_scheme->strategy == PARTITION_STRATEGY_RANGE ||
+			 part_scheme->strategy == PARTITION_STRATEGY_LIST) &&
 			clauselist == NIL)
 			break;
 
@@ -1422,7 +1421,15 @@ gen_prune_steps_from_opexps(GeneratePruningStepsContext *context,
 						righttype;
 
 			/* Look up the operator's btree/hash strategy number. */
-			if (pc->op_strategy == InvalidStrategy)
+			if (pc->op_strategy == InvalidStrategy && pc->is_null)
+			{
+				/*
+				 * When the clause contains 'IS NULL' or 'IS NOT NULL' in case of
+				 * list partitioning, forcibly set the strategy to BTEqualStrategyNumber.
+				 */
+				pc->op_strategy = BTEqualStrategyNumber;
+			}
+			else if (pc->op_strategy == InvalidStrategy)
 				get_op_opfamily_properties(pc->opno,
 										   part_scheme->partopfamily[i],
 										   false,
@@ -2324,9 +2331,36 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context,
 		if (!equal(arg, partkey))
 			return PARTCLAUSE_NOMATCH;
 
-		*clause_is_not_null = (nulltest->nulltesttype == IS_NOT_NULL);
+		if (part_scheme->strategy != PARTITION_STRATEGY_LIST)
+		{
+			*clause_is_not_null = (nulltest->nulltesttype == IS_NOT_NULL);
+			return PARTCLAUSE_MATCH_NULLNESS;
+		}
+		else
+		{
+			Const	*expr = makeConst(UNKNOWNOID, -1, InvalidOid, -2,
+									  (Datum) 0, true, false);
+			PartClauseInfo *partclause =
+				(PartClauseInfo *) palloc(sizeof(PartClauseInfo));
+
+			partclause->keyno = partkeyidx;
+			partclause->expr = (Expr *) expr;
+			partclause->is_null = true;
 
-		return PARTCLAUSE_MATCH_NULLNESS;
+			if (nulltest->nulltesttype == IS_NOT_NULL)
+			{
+				partclause->op_is_ne = true;
+				partclause->op_strategy = InvalidStrategy;
+			}
+			else
+			{
+				partclause->op_is_ne = false;
+				partclause->op_strategy = BTEqualStrategyNumber;
+			}
+
+			*pc = partclause;
+			return PARTCLAUSE_MATCH_CLAUSE;
+		}
 	}
 
 	/*
@@ -2637,6 +2671,169 @@ get_matching_hash_bounds(PartitionPruneContext *context,
 }
 
 /*
+ * get_min_and_max_off
+ *
+ * Fetches the minimum and maximum offset of the matching partitions.
+ */
+static void
+get_min_and_max_off(PartitionPruneContext *context, FmgrInfo *partsupfunc,
+					Datum *values, bool *isnulls, int nvalues, int off,
+					int *minoff, int *maxoff)
+{
+	PartitionBoundInfo	boundinfo = context->boundinfo;
+	Oid				   *partcollation = context->partcollation;
+	int					saved_off = off;
+
+	/* Find greatest bound that's smaller than the lookup value. */
+	while (off >= 1)
+	{
+		int32	cmpval =  partition_lbound_datum_cmp(partsupfunc, partcollation,
+													 boundinfo->datums[off - 1],
+													 boundinfo->isnulls[off - 1],
+													 values, isnulls, nvalues);
+
+		if (cmpval != 0)
+			break;
+
+		off--;
+	}
+
+	Assert(0 == partition_lbound_datum_cmp(partsupfunc, partcollation,
+										   boundinfo->datums[off],
+										   boundinfo->isnulls[off],
+										   values, isnulls, nvalues));
+
+	*minoff = off;
+
+	/* Find smallest bound that's greater than the lookup value. */
+	off = saved_off;
+	while (off < boundinfo->ndatums - 1)
+	{
+		int32	cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+													boundinfo->datums[off + 1],
+													boundinfo->isnulls[off + 1],
+													values, isnulls, nvalues);
+
+		if (cmpval != 0)
+			break;
+
+		off++;
+	}
+
+	Assert(0 == partition_lbound_datum_cmp(partsupfunc, partcollation,
+										   boundinfo->datums[off],
+										   boundinfo->isnulls[off],
+										   values, isnulls, nvalues));
+
+	*maxoff = off;
+	Assert(*minoff >= 0 && *maxoff >= 0);
+}
+
+/*
+ * get_min_or_max_off
+ *
+ * Fetches either minimum or maximum offset of the matching partitions
+ * depending on the value of is_min parameter.
+ */
+static int
+get_min_or_max_off(PartitionPruneContext *context, FmgrInfo *partsupfunc,
+				   Datum *values, bool *isnulls, int nvalues, int partnatts,
+				   bool is_equal, bool inclusive, int off, bool is_min)
+{
+	PartitionBoundInfo  boundinfo = context->boundinfo;
+	Oid                *partcollation = context->partcollation;
+
+	/*
+	 * Based on whether the lookup values are minimum offset or maximum
+	 * offset (is_min indicates that) and whether they are inclusive or
+	 * not, we must either include the indexes of all such bounds in the
+	 * result (that is, return off to the index of smallest/greatest such
+	 * bound) or find the smallest/greatest one that's greater/smaller than
+	 * the lookup values and return the off.
+	 */
+	if (off >= 0)
+	{
+		if (is_equal && nvalues < partnatts)
+		{
+			while (off >= 1 && off < boundinfo->ndatums - 1)
+			{
+				int32       cmpval;
+				int         nextoff;
+
+				if (is_min)
+					nextoff = inclusive ? off - 1 : off + 1;
+				else
+					nextoff = inclusive ? off + 1 : off - 1;
+
+				cmpval = partition_lbound_datum_cmp(partsupfunc, partcollation,
+													boundinfo->datums[nextoff],
+													boundinfo->isnulls[nextoff],
+													values, isnulls, nvalues);
+
+				if (cmpval != 0)
+					break;
+
+				off = nextoff;
+			}
+
+			Assert(0 == partition_lbound_datum_cmp(partsupfunc, partcollation,
+												   boundinfo->datums[off],
+												   boundinfo->isnulls[off],
+												   values, isnulls, nvalues));
+			if (is_min)
+				off = inclusive ? off : off + 1;
+			else
+				off = inclusive ? off + 1 : off;
+		}
+		else if (!is_equal || (is_min && !inclusive) || (!is_min && inclusive))
+			off = off + 1;
+		else
+			off = off;
+	}
+	else
+	{
+		if (is_min)
+			off = 0;
+		else
+			off = off + 1;
+	}
+
+	return off;
+}
+
+/*
+ * add_partitions
+ *
+ * Adds the non null partitions between minimum and maximum offset passed as
+ * input.
+ */
+static void
+add_partitions(PruneStepResult *result, bool **isnulls, int minoff, int maxoff,
+			   int ncols)
+{
+	int i = 0;
+	int j = 0;
+
+	Assert(minoff >= 0 && maxoff >= 0 && ncols > 0);
+	for (i = minoff; i < maxoff; i++)
+	{
+		bool    isadd = true;
+
+		for (j = 0; j < ncols; j++)
+		{
+			if (isnulls[i][j])
+			{
+				isadd = false;
+				break;
+			}
+		}
+
+		if (isadd)
+			result->bound_offsets = bms_add_member(result->bound_offsets, i);
+	}
+}
+
+/*
  * get_matching_list_bounds
  *		Determine the offsets of list bounds matching the specified value,
  *		according to the semantics of the given operator strategy
@@ -2658,8 +2855,8 @@ get_matching_hash_bounds(PartitionPruneContext *context,
  */
 static PruneStepResult *
 get_matching_list_bounds(PartitionPruneContext *context,
-						 StrategyNumber opstrategy, Datum value, int nvalues,
-						 FmgrInfo *partsupfunc, Bitmapset *nullkeys)
+						 StrategyNumber opstrategy, Datum *values, bool *isnulls,
+						 int nvalues, FmgrInfo *partsupfunc, Bitmapset *nullkeys)
 {
 	PruneStepResult *result = (PruneStepResult *) palloc0(sizeof(PruneStepResult));
 	PartitionBoundInfo boundinfo = context->boundinfo;
@@ -2669,26 +2866,12 @@ get_matching_list_bounds(PartitionPruneContext *context,
 	bool		is_equal;
 	bool		inclusive = false;
 	Oid		   *partcollation = context->partcollation;
+	int         partnatts = context->partnatts;
 
 	Assert(context->strategy == PARTITION_STRATEGY_LIST);
-	Assert(context->partnatts == 1);
 
 	result->scan_null = result->scan_default = false;
 
-	if (!bms_is_empty(nullkeys))
-	{
-		/*
-		 * Nulls may exist in only one partition - the partition whose
-		 * accepted set of values includes null or the default partition if
-		 * the former doesn't exist.
-		 */
-		if (partition_bound_accepts_nulls(boundinfo))
-			result->scan_null = true;
-		else
-			result->scan_default = partition_bound_has_default(boundinfo);
-		return result;
-	}
-
 	/*
 	 * If there are no datums to compare keys with, but there are partitions,
 	 * just return the default partition if one exists.
@@ -2700,7 +2883,7 @@ get_matching_list_bounds(PartitionPruneContext *context,
 	}
 
 	minoff = 0;
-	maxoff = boundinfo->ndatums - 1;
+	maxoff = boundinfo->ndatums;
 
 	/*
 	 * If there are no values to compare with the datums in boundinfo, it
@@ -2709,32 +2892,49 @@ get_matching_list_bounds(PartitionPruneContext *context,
 	 */
 	if (nvalues == 0)
 	{
-		Assert(boundinfo->ndatums > 0);
-		result->bound_offsets = bms_add_range(NULL, 0,
-											  boundinfo->ndatums - 1);
+		add_partitions(result, boundinfo->isnulls, 0, boundinfo->ndatums,
+					   context->partnatts);
 		result->scan_default = partition_bound_has_default(boundinfo);
+
 		return result;
 	}
 
 	/* Special case handling of values coming from a <> operator clause. */
 	if (opstrategy == InvalidStrategy)
 	{
+		int i = 0;
+
 		/*
 		 * First match to all bounds.  We'll remove any matching datums below.
 		 */
-		Assert(boundinfo->ndatums > 0);
-		result->bound_offsets = bms_add_range(NULL, 0,
-											  boundinfo->ndatums - 1);
+		add_partitions(result, boundinfo->isnulls, 0, boundinfo->ndatums,
+					   nvalues);
 
 		off = partition_list_bsearch(partsupfunc, partcollation, boundinfo,
-									 value, &is_equal);
+									 values, isnulls, nvalues, &is_equal);
 		if (off >= 0 && is_equal)
 		{
+			if (nvalues == partnatts)
+			{
+				/* We have a match. Remove from the result. */
+				Assert(boundinfo->indexes[off] >= 0);
+				result->bound_offsets = bms_del_member(result->bound_offsets, off);
+			}
+			else
+			{
+				/*
+				 * Since the lookup value contains only a prefix of keys,
+				 * we must find other bounds that may also match the prefix.
+				 * partition_list_bsearch() returns the offset of one of them,
+				 * find others by checking adjacent bounds.
+				 */
+				get_min_and_max_off(context, partsupfunc, values, isnulls,
+									nvalues, off, &minoff, &maxoff);
 
-			/* We have a match. Remove from the result. */
-			Assert(boundinfo->indexes[off] >= 0);
-			result->bound_offsets = bms_del_member(result->bound_offsets,
-												   off);
+				/* Remove all matching bounds from the result. */
+				for (i = minoff; i <= maxoff; i++)
+					result->bound_offsets = bms_del_member(result->bound_offsets, i);
+			}
 		}
 
 		/* Always include the default partition if any. */
@@ -2757,41 +2957,53 @@ get_matching_list_bounds(PartitionPruneContext *context,
 	switch (opstrategy)
 	{
 		case BTEqualStrategyNumber:
-			off = partition_list_bsearch(partsupfunc,
-										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
+			off = partition_list_bsearch(partsupfunc, partcollation, boundinfo,
+										 values, isnulls, nvalues, &is_equal);
+
 			if (off >= 0 && is_equal)
 			{
-				Assert(boundinfo->indexes[off] >= 0);
-				result->bound_offsets = bms_make_singleton(off);
+				if (nvalues == partnatts)
+				{
+					/* We have a match. Add to the result. */
+					Assert(boundinfo->indexes[off] >= 0);
+					result->bound_offsets = bms_make_singleton(off);
+					return result;
+				}
+				else
+				{
+					/*
+					 * Since the lookup value contains only a prefix of keys,
+					 * we must find other bounds that may also match the prefix.
+					 * partition_list_bsearch() returns the offset of one of them,
+					 * find others by checking adjacent bounds.
+					 */
+					get_min_and_max_off(context, partsupfunc, values, isnulls,
+										nvalues, off, &minoff, &maxoff);
+
+					/* Add all matching bounds to the result. */
+					result->bound_offsets = bms_add_range(NULL, minoff, maxoff);
+				}
 			}
 			else
 				result->scan_default = partition_bound_has_default(boundinfo);
+
 			return result;
 
 		case BTGreaterEqualStrategyNumber:
 			inclusive = true;
 			/* fall through */
 		case BTGreaterStrategyNumber:
-			off = partition_list_bsearch(partsupfunc,
-										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
-			if (off >= 0)
-			{
-				/* We don't want the matched datum to be in the result. */
-				if (!is_equal || !inclusive)
-					off++;
-			}
-			else
-			{
-				/*
-				 * This case means all partition bounds are greater, which in
-				 * turn means that all partitions satisfy this key.
-				 */
-				off = 0;
-			}
+			off = partition_list_bsearch(partsupfunc, partcollation, boundinfo,
+										 values, isnulls, nvalues, &is_equal);
+
+			/*
+			 * Since the lookup value contains only a prefix of keys,
+			 * we must find other bounds that may also match the prefix.
+			 * partition_list_bsearch returns the offset of one of them,
+			 * find others by checking adjacent bounds.
+			 */
+			off = get_min_or_max_off(context, partsupfunc, values, isnulls, nvalues,
+									 partnatts, is_equal, inclusive, off, true);
 
 			/*
 			 * off is greater than the numbers of datums we have partitions
@@ -2809,12 +3021,17 @@ get_matching_list_bounds(PartitionPruneContext *context,
 			inclusive = true;
 			/* fall through */
 		case BTLessStrategyNumber:
-			off = partition_list_bsearch(partsupfunc,
-										 partcollation,
-										 boundinfo, value,
-										 &is_equal);
-			if (off >= 0 && is_equal && !inclusive)
-				off--;
+			off = partition_list_bsearch(partsupfunc, partcollation, boundinfo,
+										 values, isnulls, nvalues, &is_equal);
+
+			/*
+			 * Since the lookup value contains only a prefix of keys,
+			 * we must find other bounds that may also match the prefix.
+			 * partition_list_bsearch returns the offset of one of them,
+			 * find others by checking adjacent bounds.
+			 */
+			off = get_min_or_max_off(context, partsupfunc, values, isnulls, nvalues,
+									 partnatts, is_equal, inclusive, off, false);
 
 			/*
 			 * off is smaller than the datums of all non-default partitions.
@@ -2833,8 +3050,7 @@ get_matching_list_bounds(PartitionPruneContext *context,
 			break;
 	}
 
-	Assert(minoff >= 0 && maxoff >= 0);
-	result->bound_offsets = bms_add_range(NULL, minoff, maxoff);
+	add_partitions(result, boundinfo->isnulls, minoff, maxoff, nvalues);
 	return result;
 }
 
@@ -3343,6 +3559,7 @@ perform_pruning_base_step(PartitionPruneContext *context,
 	Datum		values[PARTITION_MAX_KEYS];
 	FmgrInfo   *partsupfunc;
 	int			stateidx;
+	bool		isnulls[PARTITION_MAX_KEYS];
 
 	/*
 	 * There better be the same number of expressions and compare functions.
@@ -3364,14 +3581,16 @@ perform_pruning_base_step(PartitionPruneContext *context,
 		 * not provided in operator clauses, but instead the planner found
 		 * that they appeared in a IS NULL clause.
 		 */
-		if (bms_is_member(keyno, opstep->nullkeys))
+		if (bms_is_member(keyno, opstep->nullkeys) &&
+			context->strategy != PARTITION_STRATEGY_LIST)
 			continue;
 
 		/*
-		 * For range partitioning, we must only perform pruning with values
-		 * for either all partition keys or a prefix thereof.
+		 * For range partitioning and list partitioning, we must only perform
+		 * pruning with values for either all partition keys or a prefix thereof.
 		 */
-		if (keyno > nvalues && context->strategy == PARTITION_STRATEGY_RANGE)
+		if (keyno > nvalues && (context->strategy == PARTITION_STRATEGY_RANGE ||
+								context->strategy == PARTITION_STRATEGY_LIST))
 			break;
 
 		if (lc1 != NULL)
@@ -3389,10 +3608,11 @@ perform_pruning_base_step(PartitionPruneContext *context,
 
 			/*
 			 * Since we only allow strict operators in pruning steps, any
-			 * null-valued comparison value must cause the comparison to fail,
-			 * so that no partitions could match.
+			 * null-valued comparison value must cause the comparison to fail
+			 * in cases other than list partitioning, so that no partitions could
+			 * match.
 			 */
-			if (isnull)
+			if (isnull && context->strategy != PARTITION_STRATEGY_LIST)
 			{
 				PruneStepResult *result;
 
@@ -3405,26 +3625,35 @@ perform_pruning_base_step(PartitionPruneContext *context,
 			}
 
 			/* Set up the stepcmpfuncs entry, unless we already did */
-			cmpfn = lfirst_oid(lc2);
-			Assert(OidIsValid(cmpfn));
-			if (cmpfn != context->stepcmpfuncs[stateidx].fn_oid)
+			if (!isnull)
 			{
-				/*
-				 * If the needed support function is the same one cached in
-				 * the relation's partition key, copy the cached FmgrInfo.
-				 * Otherwise (i.e., when we have a cross-type comparison), an
-				 * actual lookup is required.
-				 */
-				if (cmpfn == context->partsupfunc[keyno].fn_oid)
-					fmgr_info_copy(&context->stepcmpfuncs[stateidx],
-								   &context->partsupfunc[keyno],
-								   context->ppccontext);
-				else
-					fmgr_info_cxt(cmpfn, &context->stepcmpfuncs[stateidx],
-								  context->ppccontext);
-			}
+				cmpfn = lfirst_oid(lc2);
+				Assert(OidIsValid(cmpfn));
+				if (cmpfn != context->stepcmpfuncs[stateidx].fn_oid)
+				{
+					/*
+					 * If the needed support function is the same one cached in
+					 * the relation's partition key, copy the cached FmgrInfo.
+					 * Otherwise (i.e., when we have a cross-type comparison), an
+					 * actual lookup is required.
+					 */
+					if (cmpfn == context->partsupfunc[keyno].fn_oid)
+						fmgr_info_copy(&context->stepcmpfuncs[stateidx],
+									   &context->partsupfunc[keyno],
+									   context->ppccontext);
+					else
+						fmgr_info_cxt(cmpfn, &context->stepcmpfuncs[stateidx],
+									  context->ppccontext);
+				}
 
-			values[keyno] = datum;
+				values[keyno] = datum;
+				isnulls[keyno] = false;
+			}
+			else
+			{
+				values[keyno] = (Datum) 0;
+				isnulls[keyno] = true;
+			}
 			nvalues++;
 
 			lc1 = lnext(opstep->exprs, lc1);
@@ -3451,7 +3680,7 @@ perform_pruning_base_step(PartitionPruneContext *context,
 		case PARTITION_STRATEGY_LIST:
 			return get_matching_list_bounds(context,
 											opstep->opstrategy,
-											values[0], nvalues,
+											values, isnulls, nvalues,
 											&partsupfunc[0],
 											opstep->nullkeys);
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 4df8cc5..75e431d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9407,10 +9407,9 @@ get_rule_expr(Node *node, deparse_context *context,
 						sep = "";
 						foreach(cell, spec->listdatums)
 						{
-							Const	   *val = lfirst_node(Const, cell);
-
 							appendStringInfoString(buf, sep);
-							get_const_expr(val, context, -1);
+							appendStringInfoString
+								(buf, get_list_partbound_value_string(lfirst(cell)));
 							sep = ", ";
 						}
 
@@ -11971,6 +11970,46 @@ flatten_reloptions(Oid relid)
 }
 
 /*
+ * get_list_partbound_value_string
+ *
+ * A C string representation of one list partition bound value
+ */
+char *
+get_list_partbound_value_string(List *bound_value)
+{
+	StringInfo  	buf = makeStringInfo();
+	StringInfo  	boundconstraint = makeStringInfo();
+	deparse_context context;
+	ListCell	   *cell = NULL;
+	char		   *sep = "";
+	int				ncols = 0;
+
+	memset(&context, 0, sizeof(deparse_context));
+	context.buf = buf;
+
+	foreach(cell, bound_value)
+	{
+		Const      *val = castNode(Const, lfirst(cell));
+
+		appendStringInfoString(buf, sep);
+		get_const_expr(val, &context, -1);
+		sep = ", ";
+		ncols++;
+	}
+
+	if (ncols > 1)
+	{
+		appendStringInfoChar(boundconstraint, '(');
+		appendStringInfoString(boundconstraint, buf->data);
+		appendStringInfoChar(boundconstraint, ')');
+
+		return boundconstraint->data;
+	}
+	else
+		return buf->data;
+}
+
+/*
  * get_range_partbound_string
  *		A C string representation of one range partition bound
  */
diff --git a/src/include/partitioning/partbounds.h b/src/include/partitioning/partbounds.h
index 9db546d..25f61c3 100644
--- a/src/include/partitioning/partbounds.h
+++ b/src/include/partitioning/partbounds.h
@@ -24,9 +24,6 @@ struct RelOptInfo;				/* avoid including pathnodes.h here */
  * descriptor, but may also be used to represent a virtual partitioned
  * table such as a partitioned joinrel within the planner.
  *
- * A list partition datum that is known to be NULL is never put into the
- * datums array. Instead, it is tracked using the null_index field.
- *
  * In the case of range partitioning, ndatums will typically be far less than
  * 2 * nparts, because a partition's upper bound and the next partition's lower
  * bound are the same in most common cases, and we only store one of them (the
@@ -38,6 +35,10 @@ struct RelOptInfo;				/* avoid including pathnodes.h here */
  * of datum-tuples with 2 datums, modulus and remainder, corresponding to a
  * given partition.
  *
+ * isnulls is an array of boolean-tuples with key->partnatts booleans values
+ * each.  Currently only used for list partitioning, it stores whether a
+ * given partition key accepts NULL as value.
+ *
  * The datums in datums array are arranged in increasing order as defined by
  * functions qsort_partition_rbound_cmp(), qsort_partition_list_value_cmp() and
  * qsort_partition_hbound_cmp() for range, list and hash partitioned tables
@@ -79,6 +80,7 @@ typedef struct PartitionBoundInfoData
 	char		strategy;		/* hash, list or range? */
 	int			ndatums;		/* Length of the datums[] array */
 	Datum	  **datums;
+	bool	  **isnulls;
 	PartitionRangeDatumKind **kind; /* The kind of each range bound datum;
 									 * NULL for hash and list partitioned
 									 * tables */
@@ -87,15 +89,15 @@ typedef struct PartitionBoundInfoData
 									 * only set for LIST partitioned tables */
 	int			nindexes;		/* Length of the indexes[] array */
 	int		   *indexes;		/* Partition indexes */
-	int			null_index;		/* Index of the null-accepting partition; -1
-								 * if there isn't one */
 	int			default_index;	/* Index of the default partition; -1 if there
 								 * isn't one */
 } PartitionBoundInfoData;
 
-#define partition_bound_accepts_nulls(bi) ((bi)->null_index != -1)
 #define partition_bound_has_default(bi) ((bi)->default_index != -1)
 
+extern bool partition_bound_accepts_nulls(PartitionBoundInfo boundinfo);
+extern int get_partition_bound_null_index(PartitionBoundInfo boundinfo);
+
 extern int	get_hash_partition_greatest_modulus(PartitionBoundInfo b);
 extern uint64 compute_partition_hash_value(int partnatts, FmgrInfo *partsupfunc,
 										   Oid *partcollation,
@@ -130,15 +132,19 @@ extern int32 partition_rbound_datum_cmp(FmgrInfo *partsupfunc,
 										Oid *partcollation,
 										Datum *rb_datums, PartitionRangeDatumKind *rb_kind,
 										Datum *tuple_datums, int n_tuple_datums);
+extern int32 partition_lbound_datum_cmp(FmgrInfo *partsupfunc,
+										Oid *partcollation,
+										Datum *lb_datums, bool *lb_isnulls,
+										Datum *values, bool *isnulls, int nvalues);
 extern int	partition_list_bsearch(FmgrInfo *partsupfunc,
 								   Oid *partcollation,
 								   PartitionBoundInfo boundinfo,
-								   Datum value, bool *is_equal);
+								   Datum *values, bool *isnulls,
+								   int nvalues, bool *is_equal);
 extern int	partition_range_datum_bsearch(FmgrInfo *partsupfunc,
 										  Oid *partcollation,
 										  PartitionBoundInfo boundinfo,
 										  int nvalues, Datum *values, bool *is_equal);
 extern int	partition_hash_bsearch(PartitionBoundInfo boundinfo,
 								   int modulus, int remainder);
-
 #endif							/* PARTBOUNDS_H */
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index d333e5e..60dac6d 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -40,6 +40,7 @@ extern List *select_rtable_names_for_explain(List *rtable,
 extern char *generate_collation_name(Oid collid);
 extern char *generate_opclass_name(Oid opclass);
 extern char *get_range_partbound_string(List *bound_datums);
+extern char *get_list_partbound_value_string(List *bound_value);
 
 extern char *pg_get_statisticsobjdef_string(Oid statextid);
 
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index 96bf426..89f1cb8 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -352,12 +352,6 @@ CREATE TABLE partitioned (
 	a int
 ) INHERITS (some_table) PARTITION BY LIST (a);
 ERROR:  cannot create partitioned table as inheritance child
--- cannot use more than 1 column as partition key for list partitioned table
-CREATE TABLE partitioned (
-	a1 int,
-	a2 int
-) PARTITION BY LIST (a1, a2);	-- fail
-ERROR:  cannot use "list" partition strategy with more than one column
 -- unsupported constraint type for partitioned tables
 CREATE TABLE partitioned (
 	a int,
@@ -919,6 +913,34 @@ CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue)
 ERROR:  partition "fail_part" would overlap partition "part10"
 LINE 1: ..._part PARTITION OF range_parted3 FOR VALUES FROM (1, minvalu...
                                                              ^
+-- now check for multi-column list partition key
+CREATE TABLE list_parted3 (
+	a int,
+	b varchar
+) PARTITION BY LIST (a, b);
+CREATE TABLE list_parted3_p1 PARTITION OF list_parted3 FOR VALUES IN ((1, 'A'));
+CREATE TABLE list_parted3_p2 PARTITION OF list_parted3 FOR VALUES IN ((1, 'B'),(1, 'E'), (1, 'E'), (2, 'C'),(2, 'D'));
+CREATE TABLE list_parted3_p3 PARTITION OF list_parted3 FOR VALUES IN ((1, NULL),(NULL, 'F'));
+CREATE TABLE list_parted3_p4 PARTITION OF list_parted3 FOR VALUES IN ((NULL, NULL));
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((1, 'E'));
+ERROR:  partition "fail_part" would overlap partition "list_parted3_p2"
+LINE 1: ...ail_part PARTITION OF list_parted3 FOR VALUES IN ((1, 'E'));
+                                                                 ^
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((1, NULL));
+ERROR:  partition "fail_part" would overlap partition "list_parted3_p3"
+LINE 1: ...il_part PARTITION OF list_parted3 FOR VALUES IN ((1, NULL));
+                                                                ^
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((NULL, 'F'));
+ERROR:  partition "fail_part" would overlap partition "list_parted3_p3"
+LINE 1: ..._part PARTITION OF list_parted3 FOR VALUES IN ((NULL, 'F'));
+                                                                 ^
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((NULL, NULL));
+ERROR:  partition "fail_part" would overlap partition "list_parted3_p4"
+LINE 1: ...part PARTITION OF list_parted3 FOR VALUES IN ((NULL, NULL));
+                                                                ^
+CREATE TABLE list_parted3_default PARTITION OF list_parted3 DEFAULT;
+-- cleanup
+DROP TABLE list_parted3;
 -- check for partition bound overlap and other invalid specifications for the hash partition
 CREATE TABLE hash_parted2 (
 	a varchar
diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out
index 5063a3d..8e704d3 100644
--- a/src/test/regress/expected/insert.out
+++ b/src/test/regress/expected/insert.out
@@ -808,6 +808,63 @@ select tableoid::regclass::text, * from mcrparted order by 1;
 
 -- cleanup
 drop table mcrparted;
+-- Test multi-column list partitioning with 3 partition keys
+create table mclparted (a int, b text, c int) partition by list (a, b, c);
+create table mclparted_p1 partition of mclparted for values in ((1, 'a', 1));
+create table mclparted_p2 partition of mclparted for values in ((1, 'a', 2), (1, 'b', 1), (2, 'a', 1));
+create table mclparted_p3 partition of mclparted for values in ((3, 'c', 3), (4, 'd', 4), (5, 'e', 5), (6, null, 6));
+create table mclparted_p4 partition of mclparted for values in ((null, 'a', 1), (1, null, 1), (1, 'a', null));
+create table mclparted_p5 partition of mclparted for values in ((null, null, null));
+-- routed to mclparted_p1
+insert into mclparted values (1, 'a', 1);
+-- routed to mclparted_p2
+insert into mclparted values (1, 'a', 2);
+insert into mclparted values (1, 'b', 1);
+insert into mclparted values (2, 'a', 1);
+-- routed to mclparted_p3
+insert into mclparted values (3, 'c', 3);
+insert into mclparted values (4, 'd', 4);
+insert into mclparted values (5, 'e', 5);
+insert into mclparted values (6, null, 6);
+-- routed to mclparted_p4
+insert into mclparted values (null, 'a', 1);
+insert into mclparted values (1, null, 1);
+insert into mclparted values (1, 'a', null);
+-- routed to mclparted_p5
+insert into mclparted values (null, null, null);
+-- error cases
+insert into mclparted values (10, 'a', 1);
+ERROR:  no partition of relation "mclparted" found for row
+DETAIL:  Partition key of the failing row contains (a, b, c) = (10, a, 1).
+insert into mclparted values (1, 'z', 1);
+ERROR:  no partition of relation "mclparted" found for row
+DETAIL:  Partition key of the failing row contains (a, b, c) = (1, z, 1).
+insert into mclparted values (1, 'a', 10);
+ERROR:  no partition of relation "mclparted" found for row
+DETAIL:  Partition key of the failing row contains (a, b, c) = (1, a, 10).
+insert into mclparted values (1, null, null);
+ERROR:  no partition of relation "mclparted" found for row
+DETAIL:  Partition key of the failing row contains (a, b, c) = (1, null, null).
+-- check rows
+select tableoid::regclass::text, * from mclparted order by 1, 2, 3, 4;
+   tableoid   | a | b | c 
+--------------+---+---+---
+ mclparted_p1 | 1 | a | 1
+ mclparted_p2 | 1 | a | 2
+ mclparted_p2 | 1 | b | 1
+ mclparted_p2 | 2 | a | 1
+ mclparted_p3 | 3 | c | 3
+ mclparted_p3 | 4 | d | 4
+ mclparted_p3 | 5 | e | 5
+ mclparted_p3 | 6 |   | 6
+ mclparted_p4 | 1 | a |  
+ mclparted_p4 | 1 |   | 1
+ mclparted_p4 |   | a | 1
+ mclparted_p5 |   |   |  
+(12 rows)
+
+-- cleanup
+drop table mclparted;
 -- check that a BR constraint can't make partition contain violating rows
 create table brtrigpartcon (a int, b text) partition by list (a);
 create table brtrigpartcon1 partition of brtrigpartcon for values in (1);
@@ -981,6 +1038,96 @@ select tableoid::regclass, * from mcrparted order by a, b;
 (11 rows)
 
 drop table mcrparted;
+-- check multi-column list partitioning with partition key constraint
+create table mclparted (a text, b int) partition by list(a, b);
+create table mclparted_p1 partition of mclparted for values in (('a', 1));
+create table mclparted_p2 partition of mclparted for values in (('a', 2), ('b', 1), ('c', 3), ('d', 3), ('e', 3));
+create table mclparted_p3 partition of mclparted for values in (('a', 3), ('a', 4), ('a', null), (null, 1));
+create table mclparted_p4 partition of mclparted for values in (('b', null), (null, 2));
+create table mclparted_p5 partition of mclparted for values in ((null, null));
+create table mclparted_p6 partition of mclparted DEFAULT;
+\d+ mclparted
+                           Partitioned table "public.mclparted"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition key: LIST (a, b)
+Partitions: mclparted_p1 FOR VALUES IN (('a', 1)),
+            mclparted_p2 FOR VALUES IN (('a', 2), ('b', 1), ('c', 3), ('d', 3), ('e', 3)),
+            mclparted_p3 FOR VALUES IN (('a', 3), ('a', 4), ('a', NULL), (NULL, 1)),
+            mclparted_p4 FOR VALUES IN (('b', NULL), (NULL, 2)),
+            mclparted_p5 FOR VALUES IN ((NULL, NULL)),
+            mclparted_p6 DEFAULT
+
+\d+ mclparted_p1
+                                Table "public.mclparted_p1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition of: mclparted FOR VALUES IN (('a', 1))
+Partition constraint: (((a = 'a'::text) AND (b = 1)))
+
+\d+ mclparted_p2
+                                Table "public.mclparted_p2"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition of: mclparted FOR VALUES IN (('a', 2), ('b', 1), ('c', 3), ('d', 3), ('e', 3))
+Partition constraint: (((a = 'a'::text) AND (b = 2)) OR ((a = 'b'::text) AND (b = 1)) OR ((a = 'c'::text) AND (b = 3)) OR ((a = 'd'::text) AND (b = 3)) OR ((a = 'e'::text) AND (b = 3)))
+
+\d+ mclparted_p3
+                                Table "public.mclparted_p3"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition of: mclparted FOR VALUES IN (('a', 3), ('a', 4), ('a', NULL), (NULL, 1))
+Partition constraint: (((a = 'a'::text) AND (b = 3)) OR ((a = 'a'::text) AND (b = 4)) OR ((a = 'a'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b = 1)))
+
+\d+ mclparted_p4
+                                Table "public.mclparted_p4"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition of: mclparted FOR VALUES IN (('b', NULL), (NULL, 2))
+Partition constraint: (((a = 'b'::text) AND (b IS NULL)) OR ((a IS NULL) AND (b = 2)))
+
+\d+ mclparted_p5
+                                Table "public.mclparted_p5"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ a      | text    |           |          |         | extended |              | 
+ b      | integer |           |          |         | plain    |              | 
+Partition of: mclparted FOR VALUES IN ((NULL, NULL))
+Partition constraint: (((a IS NULL) AND (b IS NULL)))
+
+insert into mclparted values ('a', 1), ('a', 2), ('b', 1), ('c', 3), ('d', 3),
+	('e', 3), ('a', 3), ('a', 4), ('a', null), (null, 1), ('b', null),
+	(null, 2), (null, null), ('z', 10);
+select tableoid::regclass, * from mclparted order by a, b;
+   tableoid   | a | b  
+--------------+---+----
+ mclparted_p1 | a |  1
+ mclparted_p2 | a |  2
+ mclparted_p3 | a |  3
+ mclparted_p3 | a |  4
+ mclparted_p3 | a |   
+ mclparted_p2 | b |  1
+ mclparted_p4 | b |   
+ mclparted_p2 | c |  3
+ mclparted_p2 | d |  3
+ mclparted_p2 | e |  3
+ mclparted_p6 | z | 10
+ mclparted_p3 |   |  1
+ mclparted_p4 |   |  2
+ mclparted_p5 |   |   
+(14 rows)
+
+drop table mclparted;
 -- check that wholerow vars in the RETURNING list work with partitioned tables
 create table returningwrtest (a int) partition by list (a);
 create table returningwrtest1 partition of returningwrtest for values in (1);
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 7555764..99abf2e 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -168,6 +168,438 @@ explain (costs off) select * from coll_pruning where a collate "POSIX" = 'a' col
          Filter: ((a)::text = 'a'::text COLLATE "POSIX")
 (7 rows)
 
+-- multi-column keys for list partitioning
+create table mc3lp (a int, b text, c int) partition by list (a, b, c);
+create table mc3lp_default partition of mc3lp default;
+create table mc3lp1 partition of mc3lp for values in ((1, 'a', 1), (1, 'b', 1), (5, 'e', 1));
+create table mc3lp2 partition of mc3lp for values in ((4, 'c', 4));
+create table mc3lp3 partition of mc3lp for values in ((5, 'd', 2), (5, 'e', 3), (5, 'f', 4), (8, null, 6));
+create table mc3lp4 partition of mc3lp for values in ((5, 'e', 4), (5, 'e', 5), (5, 'e', 6), (5, 'e', 7));
+create table mc3lp5 partition of mc3lp for values in ((null, 'a', 1), (1, null, 1), (5, 'g', null), (5, 'e', null));
+create table mc3lp6 partition of mc3lp for values in ((null, null, null));
+explain (costs off) select * from mc3lp where a = 4;
+        QUERY PLAN        
+--------------------------
+ Seq Scan on mc3lp2 mc3lp
+   Filter: (a = 4)
+(2 rows)
+
+explain (costs off) select * from mc3lp where a < 4;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (a < 4)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (a < 4)
+   ->  Seq Scan on mc3lp_default mc3lp_3
+         Filter: (a < 4)
+(7 rows)
+
+explain (costs off) select * from mc3lp where a <= 4;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (a <= 4)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (a <= 4)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (a <= 4)
+   ->  Seq Scan on mc3lp_default mc3lp_4
+         Filter: (a <= 4)
+(9 rows)
+
+explain (costs off) select * from mc3lp where a > 4;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (a > 4)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (a > 4)
+   ->  Seq Scan on mc3lp3 mc3lp_3
+         Filter: (a > 4)
+   ->  Seq Scan on mc3lp4 mc3lp_4
+         Filter: (a > 4)
+   ->  Seq Scan on mc3lp_default mc3lp_5
+         Filter: (a > 4)
+(11 rows)
+
+explain (costs off) select * from mc3lp where a >= 4;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (a >= 4)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (a >= 4)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (a >= 4)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (a >= 4)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (a >= 4)
+   ->  Seq Scan on mc3lp_default mc3lp_6
+         Filter: (a >= 4)
+(13 rows)
+
+explain (costs off) select * from mc3lp where a is null;
+            QUERY PLAN            
+----------------------------------
+ Append
+   ->  Seq Scan on mc3lp5 mc3lp_1
+         Filter: (a IS NULL)
+   ->  Seq Scan on mc3lp6 mc3lp_2
+         Filter: (a IS NULL)
+(5 rows)
+
+explain (costs off) select * from mc3lp where a is not null;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (a IS NOT NULL)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (a IS NOT NULL)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (a IS NOT NULL)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (a IS NOT NULL)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (a IS NOT NULL)
+   ->  Seq Scan on mc3lp_default mc3lp_6
+         Filter: (a IS NOT NULL)
+(13 rows)
+
+explain (costs off) select * from mc3lp where b = 'c';
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b = 'c'::text)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b = 'c'::text)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b < 'c';
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b < 'c'::text)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b < 'c'::text)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b <= 'c';
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b <= 'c'::text)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b <= 'c'::text)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b > 'c';
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b > 'c'::text)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b > 'c'::text)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b >= 'c';
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b >= 'c'::text)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b >= 'c'::text)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b is null;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b IS NULL)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b IS NULL)
+(15 rows)
+
+explain (costs off) select * from mc3lp where b is not null;
+               QUERY PLAN                
+-----------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp6 mc3lp_6
+         Filter: (b IS NOT NULL)
+   ->  Seq Scan on mc3lp_default mc3lp_7
+         Filter: (b IS NOT NULL)
+(15 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e';
+                  QUERY PLAN                   
+-----------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp3 mc3lp_3
+         Filter: ((a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp4 mc3lp_4
+         Filter: ((a = 5) AND (b = 'e'::text))
+(9 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b < 'e';
+               QUERY PLAN                
+-----------------------------------------
+ Seq Scan on mc3lp3 mc3lp
+   Filter: ((b < 'e'::text) AND (a = 5))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b > 'e';
+                  QUERY PLAN                   
+-----------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp5 mc3lp_1
+         Filter: ((b > 'e'::text) AND (a = 5))
+   ->  Seq Scan on mc3lp3 mc3lp_2
+         Filter: ((b > 'e'::text) AND (a = 5))
+(5 rows)
+
+explain (costs off) select * from mc3lp where a is null and b is null;
+               QUERY PLAN                
+-----------------------------------------
+ Seq Scan on mc3lp6 mc3lp
+   Filter: ((a IS NULL) AND (b IS NULL))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a is not null and b is not null;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+   ->  Seq Scan on mc3lp_default mc3lp_6
+         Filter: ((a IS NOT NULL) AND (b IS NOT NULL))
+(13 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and c = 2;
+              QUERY PLAN               
+---------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((a = 5) AND (c = 2))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((a = 5) AND (c = 2))
+   ->  Seq Scan on mc3lp3 mc3lp_3
+         Filter: ((a = 5) AND (c = 2))
+   ->  Seq Scan on mc3lp4 mc3lp_4
+         Filter: ((a = 5) AND (c = 2))
+(9 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and c < 2;
+              QUERY PLAN               
+---------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((c < 2) AND (a = 5))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((c < 2) AND (a = 5))
+   ->  Seq Scan on mc3lp3 mc3lp_3
+         Filter: ((c < 2) AND (a = 5))
+   ->  Seq Scan on mc3lp4 mc3lp_4
+         Filter: ((c < 2) AND (a = 5))
+(9 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and c > 2;
+              QUERY PLAN               
+---------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((c > 2) AND (a = 5))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((c > 2) AND (a = 5))
+   ->  Seq Scan on mc3lp3 mc3lp_3
+         Filter: ((c > 2) AND (a = 5))
+   ->  Seq Scan on mc3lp4 mc3lp_4
+         Filter: ((c > 2) AND (a = 5))
+(9 rows)
+
+explain (costs off) select * from mc3lp where a is null and c is null;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp5 mc3lp_1
+         Filter: ((a IS NULL) AND (c IS NULL))
+   ->  Seq Scan on mc3lp6 mc3lp_2
+         Filter: ((a IS NULL) AND (c IS NULL))
+(5 rows)
+
+explain (costs off) select * from mc3lp where a is not null and c is not null;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+   ->  Seq Scan on mc3lp5 mc3lp_2
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+   ->  Seq Scan on mc3lp2 mc3lp_3
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+   ->  Seq Scan on mc3lp3 mc3lp_4
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+   ->  Seq Scan on mc3lp4 mc3lp_5
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+   ->  Seq Scan on mc3lp_default mc3lp_6
+         Filter: ((a IS NOT NULL) AND (c IS NOT NULL))
+(13 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c = 4;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Seq Scan on mc3lp4 mc3lp
+   Filter: ((a = 5) AND (b = 'e'::text) AND (c = 4))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c < 4;
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((c < 4) AND (a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp3 mc3lp_2
+         Filter: ((c < 4) AND (a = 5) AND (b = 'e'::text))
+(5 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c <= 4;
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((c <= 4) AND (a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp3 mc3lp_2
+         Filter: ((c <= 4) AND (a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp4 mc3lp_3
+         Filter: ((c <= 4) AND (a = 5) AND (b = 'e'::text))
+(7 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c > 4;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Seq Scan on mc3lp4 mc3lp
+   Filter: ((c > 4) AND (a = 5) AND (b = 'e'::text))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c >= 4;
+                      QUERY PLAN                      
+------------------------------------------------------
+ Seq Scan on mc3lp4 mc3lp
+   Filter: ((c >= 4) AND (a = 5) AND (b = 'e'::text))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c is null;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Seq Scan on mc3lp5 mc3lp
+   Filter: ((c IS NULL) AND (a = 5) AND (b = 'e'::text))
+(2 rows)
+
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c is not null;
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ Append
+   ->  Seq Scan on mc3lp1 mc3lp_1
+         Filter: ((c IS NOT NULL) AND (a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp3 mc3lp_2
+         Filter: ((c IS NOT NULL) AND (a = 5) AND (b = 'e'::text))
+   ->  Seq Scan on mc3lp4 mc3lp_3
+         Filter: ((c IS NOT NULL) AND (a = 5) AND (b = 'e'::text))
+(7 rows)
+
 create table rlp (a int, b varchar) partition by range (a);
 create table rlp_default partition of rlp default partition by list (a);
 create table rlp_default_default partition of rlp_default default;
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index cc41f58..61e1129 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -342,12 +342,6 @@ CREATE TABLE partitioned (
 	a int
 ) INHERITS (some_table) PARTITION BY LIST (a);
 
--- cannot use more than 1 column as partition key for list partitioned table
-CREATE TABLE partitioned (
-	a1 int,
-	a2 int
-) PARTITION BY LIST (a1, a2);	-- fail
-
 -- unsupported constraint type for partitioned tables
 CREATE TABLE partitioned (
 	a int,
@@ -728,6 +722,25 @@ CREATE TABLE range3_default PARTITION OF range_parted3 DEFAULT;
 -- more specific ranges
 CREATE TABLE fail_part PARTITION OF range_parted3 FOR VALUES FROM (1, minvalue) TO (1, maxvalue);
 
+-- now check for multi-column list partition key
+CREATE TABLE list_parted3 (
+	a int,
+	b varchar
+) PARTITION BY LIST (a, b);
+
+CREATE TABLE list_parted3_p1 PARTITION OF list_parted3 FOR VALUES IN ((1, 'A'));
+CREATE TABLE list_parted3_p2 PARTITION OF list_parted3 FOR VALUES IN ((1, 'B'),(1, 'E'), (1, 'E'), (2, 'C'),(2, 'D'));
+CREATE TABLE list_parted3_p3 PARTITION OF list_parted3 FOR VALUES IN ((1, NULL),(NULL, 'F'));
+CREATE TABLE list_parted3_p4 PARTITION OF list_parted3 FOR VALUES IN ((NULL, NULL));
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((1, 'E'));
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((1, NULL));
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((NULL, 'F'));
+CREATE TABLE fail_part PARTITION OF list_parted3 FOR VALUES IN ((NULL, NULL));
+CREATE TABLE list_parted3_default PARTITION OF list_parted3 DEFAULT;
+
+-- cleanup
+DROP TABLE list_parted3;
+
 -- check for partition bound overlap and other invalid specifications for the hash partition
 CREATE TABLE hash_parted2 (
 	a varchar
diff --git a/src/test/regress/sql/insert.sql b/src/test/regress/sql/insert.sql
index bfaa8a3..2bfc55c 100644
--- a/src/test/regress/sql/insert.sql
+++ b/src/test/regress/sql/insert.sql
@@ -536,6 +536,48 @@ select tableoid::regclass::text, * from mcrparted order by 1;
 -- cleanup
 drop table mcrparted;
 
+-- Test multi-column list partitioning with 3 partition keys
+create table mclparted (a int, b text, c int) partition by list (a, b, c);
+create table mclparted_p1 partition of mclparted for values in ((1, 'a', 1));
+create table mclparted_p2 partition of mclparted for values in ((1, 'a', 2), (1, 'b', 1), (2, 'a', 1));
+create table mclparted_p3 partition of mclparted for values in ((3, 'c', 3), (4, 'd', 4), (5, 'e', 5), (6, null, 6));
+create table mclparted_p4 partition of mclparted for values in ((null, 'a', 1), (1, null, 1), (1, 'a', null));
+create table mclparted_p5 partition of mclparted for values in ((null, null, null));
+
+-- routed to mclparted_p1
+insert into mclparted values (1, 'a', 1);
+
+-- routed to mclparted_p2
+insert into mclparted values (1, 'a', 2);
+insert into mclparted values (1, 'b', 1);
+insert into mclparted values (2, 'a', 1);
+
+-- routed to mclparted_p3
+insert into mclparted values (3, 'c', 3);
+insert into mclparted values (4, 'd', 4);
+insert into mclparted values (5, 'e', 5);
+insert into mclparted values (6, null, 6);
+
+-- routed to mclparted_p4
+insert into mclparted values (null, 'a', 1);
+insert into mclparted values (1, null, 1);
+insert into mclparted values (1, 'a', null);
+
+-- routed to mclparted_p5
+insert into mclparted values (null, null, null);
+
+-- error cases
+insert into mclparted values (10, 'a', 1);
+insert into mclparted values (1, 'z', 1);
+insert into mclparted values (1, 'a', 10);
+insert into mclparted values (1, null, null);
+
+-- check rows
+select tableoid::regclass::text, * from mclparted order by 1, 2, 3, 4;
+
+-- cleanup
+drop table mclparted;
+
 -- check that a BR constraint can't make partition contain violating rows
 create table brtrigpartcon (a int, b text) partition by list (a);
 create table brtrigpartcon1 partition of brtrigpartcon for values in (1);
@@ -612,6 +654,28 @@ insert into mcrparted values ('aaa', 0), ('b', 0), ('bz', 10), ('c', -10),
 select tableoid::regclass, * from mcrparted order by a, b;
 drop table mcrparted;
 
+-- check multi-column list partitioning with partition key constraint
+create table mclparted (a text, b int) partition by list(a, b);
+create table mclparted_p1 partition of mclparted for values in (('a', 1));
+create table mclparted_p2 partition of mclparted for values in (('a', 2), ('b', 1), ('c', 3), ('d', 3), ('e', 3));
+create table mclparted_p3 partition of mclparted for values in (('a', 3), ('a', 4), ('a', null), (null, 1));
+create table mclparted_p4 partition of mclparted for values in (('b', null), (null, 2));
+create table mclparted_p5 partition of mclparted for values in ((null, null));
+create table mclparted_p6 partition of mclparted DEFAULT;
+
+\d+ mclparted
+\d+ mclparted_p1
+\d+ mclparted_p2
+\d+ mclparted_p3
+\d+ mclparted_p4
+\d+ mclparted_p5
+
+insert into mclparted values ('a', 1), ('a', 2), ('b', 1), ('c', 3), ('d', 3),
+	('e', 3), ('a', 3), ('a', 4), ('a', null), (null, 1), ('b', null),
+	(null, 2), (null, null), ('z', 10);
+select tableoid::regclass, * from mclparted order by a, b;
+drop table mclparted;
+
 -- check that wholerow vars in the RETURNING list work with partitioned tables
 create table returningwrtest (a int) partition by list (a);
 create table returningwrtest1 partition of returningwrtest for values in (1);
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index d70bd86..da2762e 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -34,6 +34,48 @@ explain (costs off) select * from coll_pruning where a collate "C" = 'a' collate
 -- collation doesn't match the partitioning collation, no pruning occurs
 explain (costs off) select * from coll_pruning where a collate "POSIX" = 'a' collate "POSIX";
 
+-- multi-column keys for list partitioning
+create table mc3lp (a int, b text, c int) partition by list (a, b, c);
+create table mc3lp_default partition of mc3lp default;
+create table mc3lp1 partition of mc3lp for values in ((1, 'a', 1), (1, 'b', 1), (5, 'e', 1));
+create table mc3lp2 partition of mc3lp for values in ((4, 'c', 4));
+create table mc3lp3 partition of mc3lp for values in ((5, 'd', 2), (5, 'e', 3), (5, 'f', 4), (8, null, 6));
+create table mc3lp4 partition of mc3lp for values in ((5, 'e', 4), (5, 'e', 5), (5, 'e', 6), (5, 'e', 7));
+create table mc3lp5 partition of mc3lp for values in ((null, 'a', 1), (1, null, 1), (5, 'g', null), (5, 'e', null));
+create table mc3lp6 partition of mc3lp for values in ((null, null, null));
+
+explain (costs off) select * from mc3lp where a = 4;
+explain (costs off) select * from mc3lp where a < 4;
+explain (costs off) select * from mc3lp where a <= 4;
+explain (costs off) select * from mc3lp where a > 4;
+explain (costs off) select * from mc3lp where a >= 4;
+explain (costs off) select * from mc3lp where a is null;
+explain (costs off) select * from mc3lp where a is not null;
+explain (costs off) select * from mc3lp where b = 'c';
+explain (costs off) select * from mc3lp where b < 'c';
+explain (costs off) select * from mc3lp where b <= 'c';
+explain (costs off) select * from mc3lp where b > 'c';
+explain (costs off) select * from mc3lp where b >= 'c';
+explain (costs off) select * from mc3lp where b is null;
+explain (costs off) select * from mc3lp where b is not null;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e';
+explain (costs off) select * from mc3lp where a = 5 and b < 'e';
+explain (costs off) select * from mc3lp where a = 5 and b > 'e';
+explain (costs off) select * from mc3lp where a is null and b is null;
+explain (costs off) select * from mc3lp where a is not null and b is not null;
+explain (costs off) select * from mc3lp where a = 5 and c = 2;
+explain (costs off) select * from mc3lp where a = 5 and c < 2;
+explain (costs off) select * from mc3lp where a = 5 and c > 2;
+explain (costs off) select * from mc3lp where a is null and c is null;
+explain (costs off) select * from mc3lp where a is not null and c is not null;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c = 4;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c < 4;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c <= 4;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c > 4;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c >= 4;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c is null;
+explain (costs off) select * from mc3lp where a = 5 and b = 'e' and c is not null;
+
 create table rlp (a int, b varchar) partition by range (a);
 create table rlp_default partition of rlp default partition by list (a);
 create table rlp_default_default partition of rlp_default default;
-- 
1.8.3.1

