diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 72395a5..2e50d83 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -575,11 +575,20 @@ heapgettup(HeapScanDesc scan,
 			 * forward scanners.
 			 */
 			scan->rs_syncscan = false;
-			/* start from last page of the scan */
-			if (scan->rs_startblock > 0)
-				page = scan->rs_startblock - 1;
+			
+			/* Start from last page of the scan. */
+			if (scan->rs_numblocks == InvalidBlockNumber)
+			{
+				if (scan->rs_startblock > 0)
+					page = scan->rs_startblock - 1;
+				else
+					page = scan->rs_nblocks - 1;
+			}
 			else
-				page = scan->rs_nblocks - 1;
+			{
+				page = scan->rs_startblock + scan->rs_numblocks - 1;
+			}
+
 			heapgetpage(scan, page);
 		}
 		else
@@ -876,11 +885,18 @@ heapgettup_pagemode(HeapScanDesc scan,
 			 * forward scanners.
 			 */
 			scan->rs_syncscan = false;
+
 			/* start from last page of the scan */
-			if (scan->rs_startblock > 0)
-				page = scan->rs_startblock - 1;
-			else
-				page = scan->rs_nblocks - 1;
+			if (scan->rs_numblocks == InvalidBlockNumber) {
+				if (scan->rs_startblock > 0)
+					page = scan->rs_startblock - 1;
+				else
+					page = scan->rs_nblocks - 1;
+			}
+			else {
+				page = scan->rs_startblock + scan->rs_numblocks - 1;
+			}
+
 			heapgetpage(scan, page);
 		}
 		else
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index d9deb72..b9472c0 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -859,6 +859,7 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used)
 		case T_IndexOnlyScan:
 		case T_BitmapHeapScan:
 		case T_TidScan:
+		case T_TidRangeScan:
 		case T_SubqueryScan:
 		case T_FunctionScan:
 		case T_TableFuncScan:
@@ -1005,6 +1006,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_TidScan:
 			pname = sname = "Tid Scan";
 			break;
+		case T_TidRangeScan:
+			pname = sname = "Tid Range Scan";
+			break;
 		case T_SubqueryScan:
 			pname = sname = "Subquery Scan";
 			break;
@@ -1190,22 +1194,25 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		ExplainPropertyBool("Parallel Aware", plan->parallel_aware, es);
 	}
 
-	switch (nodeTag(plan))
-	{
-		case T_SeqScan:
-		case T_SampleScan:
-		case T_BitmapHeapScan:
-		case T_SubqueryScan:
-		case T_FunctionScan:
-		case T_TableFuncScan:
-		case T_ValuesScan:
-		case T_CteScan:
-		case T_WorkTableScan:
-			ExplainScanTarget((Scan *) plan, es);
-			break;
-		case T_TidScan:
-			show_scan_direction(es, ((TidScan *) plan)->direction);
-			ExplainScanTarget((Scan *) plan, es);
+	switch (nodeTag(plan)) {
+        case T_SeqScan:
+        case T_SampleScan:
+        case T_BitmapHeapScan:
+        case T_SubqueryScan:
+        case T_FunctionScan:
+        case T_TableFuncScan:
+        case T_ValuesScan:
+        case T_CteScan:
+        case T_WorkTableScan:
+            ExplainScanTarget((Scan *) plan, es);
+            break;
+        case T_TidScan:
+        case T_TidRangeScan:
+			{
+				ScanDirection dir = IsA(plan, TidScan) ? ((TidScan *) plan)->direction : ((TidRangeScan *) plan)->direction;
+				show_scan_direction(es, dir);
+				ExplainScanTarget((Scan *) plan, es);
+			}
 			break;
 		case T_ForeignScan:
 		case T_CustomScan:
@@ -1601,6 +1608,16 @@ ExplainNode(PlanState *planstate, List *ancestors,
 											   planstate, es);
 			}
 			break;
+		case T_TidRangeScan:
+			{
+				List	   *tidquals = ((TidRangeScan *) plan)->tidquals;
+				show_scan_qual(tidquals, "TID Cond", planstate, ancestors, es);
+				show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+				if (plan->qual)
+					show_instrumentation_count("Rows Removed by Filter", 1,
+											   planstate, es);
+			}
+			break;
 		case T_ForeignScan:
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
@@ -2898,6 +2915,7 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
 		case T_IndexOnlyScan:
 		case T_BitmapHeapScan:
 		case T_TidScan:
+		case T_TidRangeScan:
 		case T_ForeignScan:
 		case T_CustomScan:
 		case T_ModifyTable:
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index cc09895..0152e31 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -28,6 +28,7 @@ OBJS = execAmi.o execCurrent.o execExpr.o execExprInterp.o \
        nodeValuesscan.o \
        nodeCtescan.o nodeNamedtuplestorescan.o nodeWorktablescan.o \
        nodeGroup.o nodeSubplan.o nodeSubqueryscan.o nodeTidscan.o \
+       nodeTidrangescan.o \
        nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o \
        nodeTableFuncscan.o
 
diff --git a/src/backend/executor/execAmi.c b/src/backend/executor/execAmi.c
index 9e78421..48ab2db 100644
--- a/src/backend/executor/execAmi.c
+++ b/src/backend/executor/execAmi.c
@@ -52,6 +52,7 @@
 #include "executor/nodeSubqueryscan.h"
 #include "executor/nodeTableFuncscan.h"
 #include "executor/nodeTidscan.h"
+#include "executor/nodeTidrangescan.h"
 #include "executor/nodeUnique.h"
 #include "executor/nodeValuesscan.h"
 #include "executor/nodeWindowAgg.h"
@@ -197,6 +198,10 @@ ExecReScan(PlanState *node)
 			ExecReScanTidScan((TidScanState *) node);
 			break;
 
+		case T_TidRangeScanState:
+			ExecReScanTidRangeScan((TidRangeScanState *) node);
+			break;
+
 		case T_SubqueryScanState:
 			ExecReScanSubqueryScan((SubqueryScanState *) node);
 			break;
@@ -520,6 +525,7 @@ ExecSupportsBackwardScan(Plan *node)
 
 		case T_SeqScan:
 		case T_TidScan:
+		case T_TidRangeScan:
 		case T_FunctionScan:
 		case T_ValuesScan:
 		case T_CteScan:
diff --git a/src/backend/executor/execProcnode.c b/src/backend/executor/execProcnode.c
index eaed9fb..dec4dac 100644
--- a/src/backend/executor/execProcnode.c
+++ b/src/backend/executor/execProcnode.c
@@ -109,6 +109,7 @@
 #include "executor/nodeSubqueryscan.h"
 #include "executor/nodeTableFuncscan.h"
 #include "executor/nodeTidscan.h"
+#include "executor/nodeTidrangescan.h"
 #include "executor/nodeUnique.h"
 #include "executor/nodeValuesscan.h"
 #include "executor/nodeWindowAgg.h"
@@ -238,6 +239,11 @@ ExecInitNode(Plan *node, EState *estate, int eflags)
 												   estate, eflags);
 			break;
 
+		case T_TidRangeScan:
+			result = (PlanState *) ExecInitTidRangeScan((TidRangeScan *) node,
+														estate, eflags);
+			break;
+
 		case T_SubqueryScan:
 			result = (PlanState *) ExecInitSubqueryScan((SubqueryScan *) node,
 														estate, eflags);
@@ -632,6 +638,10 @@ ExecEndNode(PlanState *node)
 			ExecEndTidScan((TidScanState *) node);
 			break;
 
+		case T_TidRangeScanState:
+			ExecEndTidRangeScan((TidRangeScanState *) node);
+			break;
+
 		case T_SubqueryScanState:
 			ExecEndSubqueryScan((SubqueryScanState *) node);
 			break;
diff --git a/src/backend/executor/nodeTidrangescan.c b/src/backend/executor/nodeTidrangescan.c
new file mode 100644
index 0000000..03e62d8
--- /dev/null
+++ b/src/backend/executor/nodeTidrangescan.c
@@ -0,0 +1,380 @@
+/*-------------------------------------------------------------------------
+ *
+ * nodeTidrangescan.c
+ *	  Routines to support scanning a range of tids
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/executor/nodeTidrangescan.c
+ *
+ *-------------------------------------------------------------------------
+ */
+/*
+ * INTERFACE ROUTINES
+ *
+ *		ExecTidRangeScan			scans a relation using a range of tids.
+ *		ExecInitTidRangeScan		creates and initializes state info.
+ *		ExecReScanTidRangeScan		rescans the tid relation.
+ *		ExecEndTidRangeScan			releases all storage.
+ */
+#include "postgres.h"
+
+#include "access/relscan.h"
+#include "access/sysattr.h"
+#include "catalog/pg_type.h"
+#include "executor/execdebug.h"
+#include "executor/nodeTidrangescan.h"
+#include "miscadmin.h"
+#include "optimizer/clauses.h"
+#include "storage/bufmgr.h"
+#include "utils/rel.h"
+
+static void TidRangeEvalBounds(TidRangeScanState *tidstate, BlockNumber rs_nblocks);
+static TupleTableSlot *TidRangeNext(TidRangeScanState *node);
+static bool TidRangeRecheck(TidRangeScanState *node, TupleTableSlot *slot);
+
+
+static void
+TidRangeEvalBounds(TidRangeScanState *tidstate, BlockNumber rs_nblocks)
+{
+	ExprContext *econtext = tidstate->ss.ps.ps_ExprContext;
+	ItemPointer itemptr;
+	bool        isNull;
+
+	if (tidstate->lower_expr)
+		itemptr = (ItemPointer)
+			DatumGetPointer(ExecEvalExprSwitchContext(tidstate->lower_expr,
+														econtext,
+														&isNull));
+	else
+		isNull = true;
+
+	if (!isNull)
+	{
+		tidstate->first_block = ItemPointerGetBlockNumberNoCheck(itemptr);
+		tidstate->first_tuple = ItemPointerGetOffsetNumberNoCheck(itemptr);
+
+		if (((TidRangeScan *) (tidstate->ss.ps.plan))->lower_strict)
+		{
+			tidstate->first_tuple++;
+			if (tidstate->last_tuple == 0)
+				tidstate->last_block++;
+		}
+
+		if (tidstate->first_block > 0 && tidstate->first_block >= rs_nblocks)
+		{
+			tidstate->first_block = 0;
+			tidstate->blocks_to_scan = 0;
+			return;
+		}
+	}
+	else
+	{
+		tidstate->first_block = 0;
+		tidstate->first_tuple = 0;
+	}
+	
+	Assert(tidstate->first_block == 0 || tidstate->first_block < rs_nblocks);
+
+	if (tidstate->upper_expr)
+		itemptr = (ItemPointer)
+			DatumGetPointer(ExecEvalExprSwitchContext(tidstate->upper_expr,
+														econtext,
+														&isNull));
+	else
+		isNull = true;
+
+	if (!isNull)
+	{
+		tidstate->last_block = ItemPointerGetBlockNumberNoCheck(itemptr);
+		tidstate->last_tuple = ItemPointerGetOffsetNumberNoCheck(itemptr);
+
+		if (((TidRangeScan *) (tidstate->ss.ps.plan))->upper_strict)
+		{
+			/* If decrementing the last_tuple would cause last_block to underflow, don't do it. */
+			if (tidstate->last_block == 0 && tidstate->last_tuple == 0)
+			{
+				tidstate->first_block = 0;
+				tidstate->blocks_to_scan = 0;
+				return;
+			}
+			else
+			{
+				if (tidstate->last_tuple == 0)
+					tidstate->last_block--;
+				tidstate->last_tuple--;
+			}
+		}
+	}
+	else
+	{
+		tidstate->last_block = InvalidBlockNumber;
+		tidstate->last_tuple = MaxOffsetNumber;
+	}
+	
+	tidstate->blocks_to_scan = BlockNumberIsValid(tidstate->last_block) ? (tidstate->last_block - tidstate->first_block + 1) : (rs_nblocks - tidstate->first_block);
+}
+
+/* ----------------------------------------------------------------
+ *		TidRangeNext
+ *
+ *		Retrieve a tuple from the TidRangeScan node's currentRelation
+ *		using a heap scan between the bounds in the TidRangeScanState.
+ *
+ * ----------------------------------------------------------------
+ */
+static TupleTableSlot *
+TidRangeNext(TidRangeScanState *node)
+{
+	HeapTuple       tuple;
+	HeapScanDesc    scandesc;
+	EState         *estate;
+	ScanDirection   direction;
+	TupleTableSlot *slot;
+
+	/*
+	* get information from the estate and scan state
+	*/
+	scandesc = node->ss.ss_currentScanDesc;
+	estate = node->ss.ps.state;
+	direction = estate->es_direction;
+	if (ScanDirectionIsBackward(((TidRangeScan *) node->ss.ps.plan)->direction))
+	{
+		if (ScanDirectionIsForward(direction))
+			direction = BackwardScanDirection;
+		else if (ScanDirectionIsBackward(direction))
+			direction = ForwardScanDirection;
+	}
+	slot = node->ss.ss_ScanTupleSlot;
+
+	/* compute bounds and start a new scan, if necessary */
+	if (node->first_block == InvalidBlockNumber)
+	{
+		if (scandesc == NULL)
+		{
+			scandesc = heap_beginscan_strat(node->ss.ss_currentRelation,
+											estate->es_snapshot,
+											0, NULL,
+											false, false);
+			node->ss.ss_currentScanDesc = scandesc;
+		}
+
+		TidRangeEvalBounds(node, scandesc->rs_nblocks);
+
+	    heap_setscanlimits(scandesc, node->first_block, node->blocks_to_scan);
+		printf("set scan limits to %d, %d\n", node->first_block, node->blocks_to_scan);
+	}
+
+	/*
+	* get the next tuple from the table
+	*/
+	for (;;)
+	{
+		BlockNumber block;
+		OffsetNumber offset;
+
+		tuple = heap_getnext(scandesc, direction);
+		if (!tuple)
+			break;
+
+		block = ItemPointerGetBlockNumber(&tuple->t_self);
+		offset = ItemPointerGetOffsetNumber(&tuple->t_self);
+
+		if (block == node->first_block && offset < node->first_tuple)
+			continue;
+
+		if (block == node->last_block && offset > node->last_tuple)
+			continue;
+
+		break;
+	}
+
+	/*
+	* save the tuple and the buffer returned to us by the access methods in
+	* our scan tuple slot and return the slot.  Note: we pass 'false' because
+	* tuples returned by heap_getnext() are pointers onto disk pages and were
+	* not created with palloc() and so should not be pfree()'d.  Note also
+	* that ExecStoreTuple will increment the refcount of the buffer; the
+	* refcount will not be dropped until the tuple table slot is cleared.
+	*/
+	if (tuple)
+	   ExecStoreTuple(tuple,   /* tuple to store */
+					  slot,    /* slot to store in */
+					  scandesc->rs_cbuf,   /* buffer associated with this
+											* tuple */
+					  false);  /* don't pfree this pointer */
+	else
+	   ExecClearTuple(slot);
+
+	return slot;
+}
+
+/*
+ * TidRangeRecheck -- access method routine to recheck a tuple in EvalPlanQual
+ */
+static bool
+TidRangeRecheck(TidRangeScanState *node, TupleTableSlot *slot)
+{
+	return true;
+}
+
+
+/* ----------------------------------------------------------------
+ *		ExecTidRangeScan(node)
+ *
+ *		Scans the relation using tids and returns
+ *		   the next qualifying tuple in the direction specified.
+ *		We call the ExecScan() routine and pass it the appropriate
+ *		access method functions.
+ *
+ *		Conditions:
+ *		  -- the "cursor" maintained by the AMI is positioned at the tuple
+ *			 returned previously.
+ *
+ *		Initial States:
+ *		  -- the relation indicated is opened for scanning so that the
+ *			 "cursor" is positioned before the first qualifying tuple.
+ *		  -- tidPtr is -1.
+ * ----------------------------------------------------------------
+ */
+static TupleTableSlot *
+ExecTidRangeScan(PlanState *pstate)
+{
+	TidRangeScanState *node = castNode(TidRangeScanState, pstate);
+
+	return ExecScan(&node->ss,
+					(ExecScanAccessMtd) TidRangeNext,
+					(ExecScanRecheckMtd) TidRangeRecheck);
+}
+
+/* ----------------------------------------------------------------
+ *		ExecReScanTidRangeScan(node)
+ * ----------------------------------------------------------------
+ */
+void
+ExecReScanTidRangeScan(TidRangeScanState *node)
+{
+	HeapScanDesc scan = node->ss.ss_currentScanDesc;
+
+	if (scan != NULL)
+		heap_rescan(scan,       /* scan desc */
+					NULL);      /* new scan keys */
+
+	/* mark tid range as not computed yet */
+	node->first_block = InvalidBlockNumber;
+
+	ExecScanReScan(&node->ss);
+}
+
+/* ----------------------------------------------------------------
+ *		ExecEndTidRangeScan
+ *
+ *		Releases any storage allocated through C routines.
+ *		Returns nothing.
+ * ----------------------------------------------------------------
+ */
+void
+ExecEndTidRangeScan(TidRangeScanState *node)
+{
+	HeapScanDesc scan = node->ss.ss_currentScanDesc;
+
+	/*
+	 * Free the exprcontext
+	 */
+	ExecFreeExprContext(&node->ss.ps);
+
+	/*
+	 * clear out tuple table slots
+	 */
+	ExecClearTuple(node->ss.ps.ps_ResultTupleSlot);
+	ExecClearTuple(node->ss.ss_ScanTupleSlot);
+
+	/* close heap scan */
+	if (scan != NULL)
+		heap_endscan(scan);
+
+	/*
+	 * close the heap relation.
+	 */
+	ExecCloseScanRelation(node->ss.ss_currentRelation);
+}
+
+/* ----------------------------------------------------------------
+ *		ExecInitTidRangeScan
+ *
+ *		Initializes the tid scan's state information, creates
+ *		scan keys, and opens the base and tid relations.
+ *
+ *		Parameters:
+ *		  node: TidNode node produced by the planner.
+ *		  estate: the execution state initialized in InitPlan.
+ * ----------------------------------------------------------------
+ */
+TidRangeScanState *
+ExecInitTidRangeScan(TidRangeScan *node, EState *estate, int eflags)
+{
+	TidRangeScanState *tidstate;
+	Relation	currentRelation;
+
+	/*
+	 * create state structure
+	 */
+	tidstate = makeNode(TidRangeScanState);
+	tidstate->ss.ps.plan = (Plan *) node;
+	tidstate->ss.ps.state = estate;
+	tidstate->ss.ps.ExecProcNode = ExecTidRangeScan;
+
+	/*
+	 * Miscellaneous initialization
+	 *
+	 * create expression context for node
+	 */
+	ExecAssignExprContext(estate, &tidstate->ss.ps);
+
+	/*
+	 * mark tid range as not computed yet (note that only
+	 * first_block == InvalidBlockNumber is necessary; the
+	 * others are just for consistency)
+	 */
+	tidstate->first_block = InvalidBlockNumber;
+	tidstate->first_tuple = InvalidOffsetNumber;
+	tidstate->last_block = InvalidBlockNumber;
+	tidstate->last_tuple = InvalidOffsetNumber;
+
+	/*
+	 * open the base relation and acquire appropriate lock on it.
+	 */
+	currentRelation = ExecOpenScanRelation(estate, node->scan.scanrelid, eflags);
+
+	tidstate->ss.ss_currentRelation = currentRelation;
+	tidstate->ss.ss_currentScanDesc = NULL; /* no heap scan here */
+
+	/*
+	 * get the scan type from the relation descriptor.
+	 */
+	ExecInitScanTupleSlot(estate, &tidstate->ss,
+						  RelationGetDescr(currentRelation));
+
+	/*
+	 * Initialize result slot, type and projection.
+	 */
+	ExecInitResultTupleSlotTL(estate, &tidstate->ss.ps);
+	ExecAssignScanProjectionInfo(&tidstate->ss);
+
+	/*
+	 * initialize child expressions
+	 */
+	tidstate->ss.ps.qual =
+		ExecInitQual(node->scan.plan.qual, (PlanState *) tidstate);
+
+	tidstate->lower_expr = ExecInitExpr((Expr *) node->lower_bound, &tidstate->ss.ps);
+	tidstate->upper_expr = ExecInitExpr((Expr *) node->upper_bound, &tidstate->ss.ps);
+
+	/*
+	 * all done.
+	 */
+	return tidstate;
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 5f84984..c438058 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -588,6 +588,30 @@ _copyTidScan(const TidScan *from)
 	return newnode;
 }
 
+static TidRangeScan *
+_copyTidRangeScan(const TidRangeScan *from)
+{
+	TidRangeScan    *newnode = makeNode(TidRangeScan);
+
+	/*
+	 * copy node superclass fields
+	 */
+	CopyScanFields((const Scan *) from, (Scan *) newnode);
+
+	/*
+	 * copy remainder of node
+	 */
+	COPY_NODE_FIELD(tidquals);
+	COPY_NODE_FIELD(lower_bound);
+	COPY_NODE_FIELD(upper_bound);
+	COPY_SCALAR_FIELD(lower_strict);
+	COPY_SCALAR_FIELD(upper_strict);
+	COPY_SCALAR_FIELD(direction);
+
+	return newnode;
+}
+
+
 /*
  * _copySubqueryScan
  */
@@ -4842,6 +4866,9 @@ copyObjectImpl(const void *from)
 		case T_TidScan:
 			retval = _copyTidScan(from);
 			break;
+		case T_TidRangeScan:
+			retval = _copyTidRangeScan(from);
+			break;
 		case T_SubqueryScan:
 			retval = _copySubqueryScan(from);
 			break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 870dd2e..2cab724 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -619,6 +619,21 @@ _outTidScan(StringInfo str, const TidScan *node)
 }
 
 static void
+_outTidRangeScan(StringInfo str, const TidRangeScan *node)
+{
+	WRITE_NODE_TYPE("TIDRANGESCAN");
+
+	_outScanInfo(str, (const Scan *) node);
+
+	WRITE_NODE_FIELD(tidquals);
+	WRITE_NODE_FIELD(lower_bound);
+	WRITE_NODE_FIELD(upper_bound);
+	WRITE_BOOL_FIELD(lower_strict);
+	WRITE_BOOL_FIELD(upper_strict);
+	WRITE_ENUM_FIELD(direction, ScanDirection);
+}
+
+static void
 _outSubqueryScan(StringInfo str, const SubqueryScan *node)
 {
 	WRITE_NODE_TYPE("SUBQUERYSCAN");
@@ -1892,6 +1907,12 @@ _outTidPath(StringInfo str, const TidPath *node)
 	_outPathInfo(str, (const Path *) node);
 
 	WRITE_NODE_FIELD(tidquals);
+	WRITE_ENUM_FIELD(method, TidPathMethod);
+	WRITE_NODE_FIELD(lower_bound);
+	WRITE_NODE_FIELD(upper_bound);
+	WRITE_BOOL_FIELD(lower_strict);
+	WRITE_BOOL_FIELD(upper_strict);
+	WRITE_ENUM_FIELD(direction, ScanDirection);
 }
 
 static void
@@ -3763,6 +3784,9 @@ outNode(StringInfo str, const void *obj)
 			case T_TidScan:
 				_outTidScan(str, obj);
 				break;
+			case T_TidRangeScan:
+				_outTidRangeScan(str, obj);
+				break;
 			case T_SubqueryScan:
 				_outSubqueryScan(str, obj);
 				break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 2317f58..76c65a0 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1834,6 +1834,26 @@ _readTidScan(void)
 }
 
 /*
+ * _readTidRangeScan
+ */
+static TidRangeScan *
+_readTidRangeScan(void)
+{
+	READ_LOCALS(TidRangeScan);
+
+	ReadCommonScan(&local_node->scan);
+
+	READ_NODE_FIELD(tidquals);
+	READ_NODE_FIELD(lower_bound);
+	READ_NODE_FIELD(upper_bound);
+	READ_BOOL_FIELD(lower_strict);
+	READ_BOOL_FIELD(upper_strict);
+	READ_ENUM_FIELD(direction, ScanDirection);
+
+	READ_DONE();
+}
+
+/*
  * _readSubqueryScan
  */
 static SubqueryScan *
@@ -2684,6 +2704,8 @@ parseNodeString(void)
 		return_value = _readBitmapHeapScan();
 	else if (MATCH("TIDSCAN", 7))
 		return_value = _readTidScan();
+	else if (MATCH("TIDRANGESCAN", 12))
+		return_value = _readTidRangeScan();
 	else if (MATCH("SUBQUERYSCAN", 12))
 		return_value = _readSubqueryScan();
 	else if (MATCH("FUNCTIONSCAN", 12))
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 7bf67a0..4a26187 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -76,6 +76,7 @@
 #include "access/amapi.h"
 #include "access/htup_details.h"
 #include "access/tsmapi.h"
+#include "catalog/pg_operator.h"
 #include "executor/executor.h"
 #include "executor/nodeHash.h"
 #include "miscadmin.h"
@@ -93,6 +94,7 @@
 #include "utils/selfuncs.h"
 #include "utils/spccache.h"
 #include "utils/tuplesort.h"
+#include "nodes/print.h"
 
 
 #define LOG2(x)  (log(x) / 0.693147180559945)
@@ -1166,6 +1168,54 @@ cost_bitmap_or_node(BitmapOrPath *path, PlannerInfo *root)
 	path->path.total_cost = totalCost;
 }
 
+static void
+estimate_tidscan_tuples_and_pages(RelOptInfo *baserel, List *tidquals, TidPathMethod method, Expr *lower_bound, Expr *upper_bound,
+								  int *ntuples_out, int *npages_out, int *nrandom_pages_out, bool *isCurrentOf) {
+	ListCell   *l;
+	int ntuples = 0;
+	int npages = 0;
+	int nrandom_pages = 0;
+
+	if (method == TID_PATH_LIST)
+	{
+		foreach(l, tidquals)
+		{
+			if (IsA(lfirst(l), ScalarArrayOpExpr))
+			{
+				/* Each element of the array yields 1 tuple */
+				ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) lfirst(l);
+				Node	   *arraynode = (Node *) lsecond(saop->args);
+
+				ntuples += estimate_array_length(arraynode);
+				nrandom_pages++;
+			}
+			else if (IsA(lfirst(l), CurrentOfExpr))
+			{
+				/* CURRENT OF yields 1 tuple */
+				*isCurrentOf = true;
+				ntuples++;
+				nrandom_pages++;
+			}
+			else
+			{
+				/* It's just CTID = something, count 1 tuple */
+				ntuples++;
+				nrandom_pages++;
+			}
+		}
+	}
+	else
+	{
+		double selectivity = tid_range_selectivity(baserel, lower_bound, upper_bound);
+		ntuples += selectivity * baserel->tuples;
+		npages += selectivity * baserel->pages;
+	}
+
+	*ntuples_out = ntuples;
+	*npages_out = npages;
+	*nrandom_pages_out = nrandom_pages;
+}
+
 /*
  * cost_tidscan
  *	  Determines and returns the cost of scanning a relation using TIDs.
@@ -1176,7 +1226,8 @@ cost_bitmap_or_node(BitmapOrPath *path, PlannerInfo *root)
  */
 void
 cost_tidscan(Path *path, PlannerInfo *root,
-			 RelOptInfo *baserel, List *tidquals, ParamPathInfo *param_info)
+			 RelOptInfo *baserel, List *tidquals, TidPathMethod method, Expr *lower_bound, Expr *upper_bound,
+			 ParamPathInfo *param_info)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -1185,8 +1236,10 @@ cost_tidscan(Path *path, PlannerInfo *root,
 	Cost		cpu_per_tuple;
 	QualCost	tid_qual_cost;
 	int			ntuples;
-	ListCell   *l;
+	int			npages;
+	int			nrandom_pages;
 	double		spc_random_page_cost;
+	double		spc_seq_page_cost;
 
 	/* Should only be applied to base relations */
 	Assert(baserel->relid > 0);
@@ -1199,29 +1252,8 @@ cost_tidscan(Path *path, PlannerInfo *root,
 		path->rows = baserel->rows;
 
 	/* Count how many tuples we expect to retrieve */
-	ntuples = 0;
-	foreach(l, tidquals)
-	{
-		if (IsA(lfirst(l), ScalarArrayOpExpr))
-		{
-			/* Each element of the array yields 1 tuple */
-			ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) lfirst(l);
-			Node	   *arraynode = (Node *) lsecond(saop->args);
-
-			ntuples += estimate_array_length(arraynode);
-		}
-		else if (IsA(lfirst(l), CurrentOfExpr))
-		{
-			/* CURRENT OF yields 1 tuple */
-			isCurrentOf = true;
-			ntuples++;
-		}
-		else
-		{
-			/* It's just CTID = something, count 1 tuple */
-			ntuples++;
-		}
-	}
+	estimate_tidscan_tuples_and_pages(baserel, tidquals, method, lower_bound, upper_bound,
+									  &ntuples, &npages, &nrandom_pages, &isCurrentOf);
 
 	/*
 	 * We must force TID scan for WHERE CURRENT OF, because only nodeTidscan.c
@@ -1248,10 +1280,11 @@ cost_tidscan(Path *path, PlannerInfo *root,
 	/* fetch estimated page cost for tablespace containing table */
 	get_tablespace_page_costs(baserel->reltablespace,
 							  &spc_random_page_cost,
-							  NULL);
+							  &spc_seq_page_cost);
 
-	/* disk costs --- assume each tuple on a different page */
-	run_cost += spc_random_page_cost * ntuples;
+	/* disk costs */
+	run_cost += spc_random_page_cost * nrandom_pages;
+	run_cost += spc_seq_page_cost * npages;
 
 	/* Add scanning CPU costs */
 	get_restriction_qual_cost(root, baserel, param_info, &qpqual_cost);
diff --git a/src/backend/optimizer/path/tidpath.c b/src/backend/optimizer/path/tidpath.c
index 7a40700..28f455b 100644
--- a/src/backend/optimizer/path/tidpath.c
+++ b/src/backend/optimizer/path/tidpath.c
@@ -44,34 +44,43 @@
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 #include "optimizer/restrictinfo.h"
+#include "nodes/makefuncs.h"
 
 
-static bool IsTidEqualClause(OpExpr *node, int varno);
 static bool IsTidEqualAnyClause(ScalarArrayOpExpr *node, int varno);
 static List *TidQualFromExpr(Node *expr, int varno);
-static List *TidQualFromBaseRestrictinfo(RelOptInfo *rel);
+static List *TidQualFromBaseRestrictinfo(RelOptInfo *rel, TidPathMethod *method, Expr **lower_bound, Expr **upper_bound, bool *lower_strict, bool *upper_strict);
 
 
+static bool IsTidVar(Var *var, int varno)
+{
+	return (var->varattno == SelfItemPointerAttributeNumber &&
+			var->vartype == TIDOID &&
+			var->varno == varno &&
+			var->varlevelsup == 0);
+}
+
 /*
  * Check to see if an opclause is of the form
- *		CTID = pseudoconstant
+ *		CTID OP pseudoconstant
  * or
- *		pseudoconstant = CTID
+ *		pseudoconstant OP CTID
+ * where OP is the expected comparison operator.
  *
  * We check that the CTID Var belongs to relation "varno".  That is probably
  * redundant considering this is only applied to restriction clauses, but
  * let's be safe.
  */
 static bool
-IsTidEqualClause(OpExpr *node, int varno)
+IsTidComparison(OpExpr *node, int varno, Oid expected_comparison_operator)
 {
 	Node	   *arg1,
 			   *arg2,
 			   *other;
 	Var		   *var;
 
-	/* Operator must be tideq */
-	if (node->opno != TIDEqualOperator)
+	/* Operator must be the expected one */
+	if (node->opno != expected_comparison_operator)
 		return false;
 	if (list_length(node->args) != 2)
 		return false;
@@ -110,6 +119,14 @@ IsTidEqualClause(OpExpr *node, int varno)
 	return true;				/* success */
 }
 
+
+#define IsTidEqualClause(node, varno)	IsTidComparison(node, varno, TIDEqualOperator)
+#define IsTidLTClause(node, varno)	IsTidComparison(node, varno, TIDLessOperator)
+#define IsTidLEClause(node, varno)	IsTidComparison(node, varno, TIDLessEqOperator)
+#define IsTidGTClause(node, varno)	IsTidComparison(node, varno, TIDGreaterOperator)
+#define IsTidGEClause(node, varno)	IsTidComparison(node, varno, TIDGreaterEqOperator)
+
+
 /*
  * Check to see if a clause is of the form
  *		CTID = ANY (pseudoconstant_array)
@@ -216,14 +233,60 @@ TidQualFromExpr(Node *expr, int varno)
 	return rlst;
 }
 
+static Node *
+TidRangeQualFromExpr(Node *expr, int varno, bool want_lower_bound, Expr **bound, bool *strict)
+{
+	if (is_opclause(expr))
+	{
+		if (IsTidLTClause((OpExpr *) expr, varno) || IsTidLEClause((OpExpr *) expr, varno) ||
+			(IsTidGTClause((OpExpr *) expr, varno) || IsTidGEClause((OpExpr *) expr, varno)))
+		{
+			bool is_lower_bound = IsTidGTClause((OpExpr *) expr, varno) || IsTidGEClause((OpExpr *) expr, varno);
+
+			Node *rightop = get_rightop((Expr *) expr);
+			Node *leftop = get_leftop((Expr *) expr);
+			Node *value = rightop;
+
+			if (!IsA(leftop, Var) || !IsTidVar((Var *) leftop, varno))
+			{
+				is_lower_bound = !is_lower_bound;
+				value = leftop;
+			}
+
+			if (is_lower_bound == want_lower_bound)
+			{
+				*strict = IsTidGTClause((OpExpr *) expr, varno) || IsTidLTClause((OpExpr *) expr, varno);
+				*bound = (Expr *) value;
+				return expr;
+			}
+		}
+	}
+
+	return NULL;
+}
+
+static List *
+MakeTidRangeQuals(Node *lower_bound_expr, Node *upper_bound_expr)
+{
+	if (lower_bound_expr && !upper_bound_expr)
+		return list_make1(lower_bound_expr);
+	else if (!lower_bound_expr && upper_bound_expr)
+		return list_make1(upper_bound_expr);
+	else
+		return list_make2(lower_bound_expr, upper_bound_expr);
+}
+
 /*
  *	Extract a set of CTID conditions from the rel's baserestrictinfo list
  */
 static List *
-TidQualFromBaseRestrictinfo(RelOptInfo *rel)
+TidQualFromBaseRestrictinfo(RelOptInfo *rel, TidPathMethod *method,
+							Expr **lower_bound, Expr **upper_bound, bool *lower_strict, bool *upper_strict)
 {
 	List	   *rlst = NIL;
 	ListCell   *l;
+	Node *lower_bound_expr = NULL;
+	Node *upper_bound_expr = NULL;
 
 	foreach(l, rel->baserestrictinfo)
 	{
@@ -236,13 +299,37 @@ TidQualFromBaseRestrictinfo(RelOptInfo *rel)
 		if (!restriction_is_securely_promotable(rinfo, rel))
 			continue;
 
+		/*
+		 * Check if this clause contains a range qual
+		 */
+		if (!lower_bound_expr)
+			lower_bound_expr = TidRangeQualFromExpr((Node *) rinfo->clause, rel->relid, true, lower_bound, lower_strict);
+
+		if (!upper_bound_expr)
+			upper_bound_expr = TidRangeQualFromExpr((Node *) rinfo->clause, rel->relid, false, upper_bound, upper_strict);
+
 		rlst = TidQualFromExpr((Node *) rinfo->clause, rel->relid);
 		if (rlst)
 			break;
 	}
+
+	/*
+	 * If one or both range quals was specified, and there were no equality/in/current-of quals, use them.
+	 */
+	if (!rlst && (lower_bound_expr || upper_bound_expr))
+	{
+		rlst = MakeTidRangeQuals(lower_bound_expr, upper_bound_expr);
+		*method = TID_PATH_RANGE;
+	}
+	else if (rlst)
+	{
+		*method = TID_PATH_LIST;
+	}
+
 	return rlst;
 }
 
+
 /*
  * create_tidscan_paths
  *	  Create paths corresponding to direct TID scans of the given rel.
@@ -254,8 +341,15 @@ TidQualFromBaseRestrictinfo(RelOptInfo *rel)
 void
 create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel)
 {
-	Relids		required_outer;
-	List	   *tidquals;
+	Relids		   required_outer;
+	List		  *tidquals;
+	TidPathMethod  method = TID_PATH_RANGE;
+	Expr		  *lower_bound = NULL;
+	Expr		  *upper_bound = NULL;
+	bool		   lower_strict = false;
+	bool		   upper_strict = false;
+	List			*pathkeys = NULL;
+	ScanDirection	 direction = ForwardScanDirection;
 
 	/*
 	 * We don't support pushing join clauses into the quals of a tidscan, but
@@ -264,33 +358,42 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel)
 	 */
 	required_outer = rel->lateral_relids;
 
-	tidquals = TidQualFromBaseRestrictinfo(rel);
+	tidquals = TidQualFromBaseRestrictinfo(rel, &method, &lower_bound, &upper_bound, &lower_strict, &upper_strict);
 
-	if (tidquals)
+	/*
+	 * Try to determine the best scan direction and create some useful pathkeys.
+	 */
+	if (has_useful_pathkeys(root, rel))
 	{
-		List			*pathkeys = NULL;
-		ScanDirection	 direction = ForwardScanDirection;
-
-		if (has_useful_pathkeys(root, rel)) {
-			/*
-			 * Build path keys corresponding to ORDER BY ctid ASC, and check
-			 * whether they will be useful for this scan.  If not, build
-			 * path keys for DESC, and try that; set the direction to
-			 * BackwardScanDirection if so.  If neither of them will be
-			 * useful, no path keys will be set.
-			 */
-			pathkeys = build_tidscan_pathkeys(root, rel, ForwardScanDirection);
-			if (!pathkeys_contained_in(pathkeys, root->query_pathkeys))
-			{
-				pathkeys = build_tidscan_pathkeys(root, rel, BackwardScanDirection);
-				if (pathkeys_contained_in(pathkeys, root->query_pathkeys))
-					direction = BackwardScanDirection;
-				else
-					pathkeys = NULL;
-			}
+		/*
+		 * Build path keys corresponding to ORDER BY ctid ASC, and check
+		 * whether they will be useful for this scan.  If not, build
+		 * path keys for DESC, and try that; set the direction to
+		 * BackwardScanDirection if so.  If neither of them will be
+		 * useful, no path keys will be set.
+		 */
+		pathkeys = build_tidscan_pathkeys(root, rel, ForwardScanDirection);
+		if (!pathkeys_contained_in(pathkeys, root->query_pathkeys))
+		{
+			pathkeys = build_tidscan_pathkeys(root, rel, BackwardScanDirection);
+			if (pathkeys_contained_in(pathkeys, root->query_pathkeys))
+				direction = BackwardScanDirection;
+			else
+				pathkeys = NULL;
 		}
+	}
+
+	/*
+	 * If there are tidquals or some useful pathkeys were found, then it's
+	 * worth generating a tidscan path.
+	 */
+	if (tidquals || pathkeys)
+	{
+		/* If we don't have any tidquals, then we MUST create a tid range scan path. */
+		Assert(tidquals || method == TID_PATH_RANGE);
 
-		add_path(rel, (Path *) create_tidscan_path(root, rel, tidquals, pathkeys, direction,
-												   required_outer));
+		add_path(rel, (Path *) create_tidscan_path(root, rel, tidquals,
+												   method, lower_bound, upper_bound, lower_strict, upper_strict,
+												   required_outer, direction, pathkeys));
 	}
 }
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 4e1faa6..e479a0a 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -125,8 +125,8 @@ static Plan *create_bitmap_subplan(PlannerInfo *root, Path *bitmapqual,
 					  List **qual, List **indexqual, List **indexECs);
 static void bitmap_subplan_mark_shared(Plan *plan);
 static List *flatten_partitioned_rels(List *partitioned_rels);
-static TidScan *create_tidscan_plan(PlannerInfo *root, TidPath *best_path,
-					List *tlist, List *scan_clauses);
+static Scan *create_tidscan_plan(PlannerInfo *root, TidPath *best_path,
+						 List *tlist, List *scan_clauses);
 static SubqueryScan *create_subqueryscan_plan(PlannerInfo *root,
 						 SubqueryScanPath *best_path,
 						 List *tlist, List *scan_clauses);
@@ -186,6 +186,8 @@ static BitmapHeapScan *make_bitmap_heapscan(List *qptlist,
 					 Index scanrelid);
 static TidScan *make_tidscan(List *qptlist, List *qpqual, Index scanrelid,
 			 List *tidquals, ScanDirection direction);
+static TidRangeScan *make_tidrangescan(List *qptlist, List *qpqual, Index scanrelid,
+			 List *tidquals, Expr *lower_bound, Expr *upper_bound, bool lower_strict, bool upper_strict, ScanDirection direction);
 static SubqueryScan *make_subqueryscan(List *qptlist,
 				  List *qpqual,
 				  Index scanrelid,
@@ -371,6 +373,7 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
 		case T_IndexOnlyScan:
 		case T_BitmapHeapScan:
 		case T_TidScan:
+		case T_TidRangeScan:
 		case T_SubqueryScan:
 		case T_FunctionScan:
 		case T_TableFuncScan:
@@ -648,6 +651,7 @@ create_scan_plan(PlannerInfo *root, Path *best_path, int flags)
 			break;
 
 		case T_TidScan:
+		case T_TidRangeScan:
 			plan = (Plan *) create_tidscan_plan(root,
 												(TidPath *) best_path,
 												tlist,
@@ -3057,11 +3061,11 @@ create_bitmap_subplan(PlannerInfo *root, Path *bitmapqual,
  *	 Returns a tidscan plan for the base relation scanned by 'best_path'
  *	 with restriction clauses 'scan_clauses' and targetlist 'tlist'.
  */
-static TidScan *
+static Scan *
 create_tidscan_plan(PlannerInfo *root, TidPath *best_path,
 					List *tlist, List *scan_clauses)
 {
-	TidScan    *scan_plan;
+	Scan	   *scan_plan;
 	Index		scan_relid = best_path->path.parent->relid;
 	List	   *tidquals = best_path->tidquals;
 	List	   *ortidquals;
@@ -3090,18 +3094,34 @@ create_tidscan_plan(PlannerInfo *root, TidPath *best_path,
 	 * tidquals list has implicit OR semantics.
 	 */
 	ortidquals = tidquals;
-	if (list_length(ortidquals) > 1)
+	if (best_path->method == TID_PATH_LIST && list_length(ortidquals) > 1)
 		ortidquals = list_make1(make_orclause(ortidquals));
 	scan_clauses = list_difference(scan_clauses, ortidquals);
 
-	scan_plan = make_tidscan(tlist,
-							 scan_clauses,
-							 scan_relid,
-							 tidquals,
-							 best_path->direction
-							);
+	if (best_path->method == TID_PATH_LIST)
+	{
+        scan_plan = make_tidscan(tlist,
+                                 scan_clauses,
+                                 scan_relid,
+                                 tidquals,
+                                 best_path->direction
+        );
+	}
+	else
+	{
+		scan_plan = (Scan *) make_tidrangescan(tlist,
+											   scan_clauses,
+											   scan_relid,
+											   tidquals,
+											   best_path->lower_bound,
+											   best_path->upper_bound,
+											   best_path->lower_strict,
+											   best_path->upper_strict,
+											   best_path->direction
+  											);
+	}
 
-	copy_generic_path_info(&scan_plan->scan.plan, &best_path->path);
+	copy_generic_path_info(&scan_plan->plan, &best_path->path);
 
 	return scan_plan;
 }
@@ -5198,6 +5218,36 @@ make_tidscan(List *qptlist,
 	return node;
 }
 
+static TidRangeScan *
+make_tidrangescan(List *qptlist,
+				  List *qpqual,
+				  Index scanrelid,
+				  List *tidquals,
+				  Expr *lower_bound,
+				  Expr *upper_bound,
+				  bool lower_strict,
+				  bool upper_strict,
+				  ScanDirection direction
+				 )
+{
+	TidRangeScan    *node = makeNode(TidRangeScan);
+	Plan	   *plan = &node->scan.plan;
+
+	plan->targetlist = qptlist;
+	plan->qual = qpqual;
+	plan->lefttree = NULL;
+	plan->righttree = NULL;
+	node->scan.scanrelid = scanrelid;
+	node->tidquals = tidquals;
+	node->lower_bound = lower_bound;
+	node->upper_bound = upper_bound;
+	node->lower_strict = lower_strict;
+	node->upper_strict = upper_strict;
+	node->direction = direction;
+
+	return node;
+}
+
 static SubqueryScan *
 make_subqueryscan(List *qptlist,
 				  List *qpqual,
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 69dd327..854d2d6 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -541,6 +541,21 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 					fix_scan_list(root, splan->tidquals, rtoffset);
 			}
 			break;
+		case T_TidRangeScan:
+			{
+				TidRangeScan    *splan = (TidRangeScan *) plan;
+
+				splan->scan.scanrelid += rtoffset;
+				splan->scan.plan.targetlist =
+					fix_scan_list(root, splan->scan.plan.targetlist, rtoffset);
+				splan->scan.plan.qual =
+					fix_scan_list(root, splan->scan.plan.qual, rtoffset);
+				splan->lower_bound =
+					(OpExpr *) fix_scan_expr(root, (Node *) splan->lower_bound, rtoffset);
+				splan->upper_bound =
+					(OpExpr *) fix_scan_expr(root, (Node *) splan->upper_bound, rtoffset);
+			}
+			break;
 		case T_SubqueryScan:
 			/* Needs special treatment, see comments below */
 			return set_subqueryscan_references(root,
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index 83008d7..2b917694 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -2410,6 +2410,14 @@ finalize_plan(PlannerInfo *root, Plan *plan,
 			context.paramids = bms_add_members(context.paramids, scan_params);
 			break;
 
+		case T_TidRangeScan:
+			finalize_primnode((Node *) ((TidRangeScan *) plan)->lower_bound,
+							  &context);
+			finalize_primnode((Node *) ((TidRangeScan *) plan)->upper_bound,
+							  &context);
+			context.paramids = bms_add_members(context.paramids, scan_params);
+			break;
+
 		case T_SubqueryScan:
 			{
 				SubqueryScan *sscan = (SubqueryScan *) plan;
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index e2d51a9..fb8b81f 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1186,12 +1186,12 @@ create_bitmap_or_path(PlannerInfo *root,
  */
 TidPath *
 create_tidscan_path(PlannerInfo *root, RelOptInfo *rel, List *tidquals,
-					List *pathkeys, ScanDirection direction,
-					Relids required_outer)
+					TidPathMethod method, Expr *lower_bound, Expr *upper_bound, bool lower_strict, bool upper_strict,
+					Relids required_outer, ScanDirection direction, List *pathkeys)
 {
 	TidPath    *pathnode = makeNode(TidPath);
 
-	pathnode->path.pathtype = T_TidScan;
+	pathnode->path.pathtype = (method == TID_PATH_LIST) ? T_TidScan : T_TidRangeScan;
 	pathnode->path.parent = rel;
 	pathnode->path.pathtarget = rel->reltarget;
 	pathnode->path.param_info = get_baserel_parampathinfo(root, rel,
@@ -1202,9 +1202,14 @@ create_tidscan_path(PlannerInfo *root, RelOptInfo *rel, List *tidquals,
 	pathnode->path.pathkeys = pathkeys;
 
 	pathnode->tidquals = tidquals;
+	pathnode->method = method;
+	pathnode->lower_bound = lower_bound;
+	pathnode->upper_bound = upper_bound;
+	pathnode->lower_strict = lower_strict;
+	pathnode->upper_strict = upper_strict;
 	pathnode->direction = direction;
 
-	cost_tidscan(&pathnode->path, root, rel, tidquals,
+	cost_tidscan(&pathnode->path, root, rel, tidquals, method, lower_bound, upper_bound,
 				 pathnode->path.param_info);
 
 	return pathnode;
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index f1c78ff..8c87d28 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -8219,3 +8219,27 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 
 	*indexPages = index->pages;
 }
+
+static BlockNumber
+get_block_number_from_tid_qual(Expr *qual, BlockNumber default_when_missing)
+{
+	if (qual && IsA(qual, Const)) {
+		Const *con = (Const *) qual;
+		ItemPointer itemptr = (ItemPointer) DatumGetPointer(con->constvalue);
+		return ItemPointerGetBlockNumberNoCheck(itemptr);
+	}
+	else
+	{
+		return default_when_missing;
+	}
+}
+
+double
+tid_range_selectivity(RelOptInfo *rel, Expr *lower_qual, Expr *upper_qual)
+{
+	BlockNumber lower_block = get_block_number_from_tid_qual(lower_qual, 0);
+	BlockNumber upper_block = get_block_number_from_tid_qual(upper_qual, rel->pages);
+
+	double selectivity = (upper_block - lower_block) / ((double) rel->pages + 1);
+	return Max(0.0, Min(1.0, selectivity));
+}
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 31e7d61..cdd2cd3 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -160,11 +160,11 @@
   oprname => '>', oprleft => 'tid', oprright => 'tid', oprresult => 'bool',
   oprcom => '<(tid,tid)', oprnegate => '<=(tid,tid)', oprcode => 'tidgt',
   oprrest => 'scalargtsel', oprjoin => 'scalargtjoinsel' },
-{ oid => '2801', descr => 'less than or equal',
+{ oid => '2801', oid_symbol => 'TIDLessEqOperator', descr => 'less than or equal',
   oprname => '<=', oprleft => 'tid', oprright => 'tid', oprresult => 'bool',
   oprcom => '>=(tid,tid)', oprnegate => '>(tid,tid)', oprcode => 'tidle',
   oprrest => 'scalarlesel', oprjoin => 'scalarlejoinsel' },
-{ oid => '2802', descr => 'greater than or equal',
+{ oid => '2802', oid_symbol => 'TIDGreaterEqOperator', descr => 'greater than or equal',
   oprname => '>=', oprleft => 'tid', oprright => 'tid', oprresult => 'bool',
   oprcom => '<=(tid,tid)', oprnegate => '<(tid,tid)', oprcode => 'tidge',
   oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' },
diff --git a/src/include/executor/nodeTidrangescan.h b/src/include/executor/nodeTidrangescan.h
new file mode 100644
index 0000000..d5ad2e1
--- /dev/null
+++ b/src/include/executor/nodeTidrangescan.h
@@ -0,0 +1,23 @@
+/*-------------------------------------------------------------------------
+ *
+ * nodeTidrangescan.h
+ *
+ *
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/executor/nodeTidscan.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef NODETIDRANGESCAN_H
+#define NODETIDRANGESCAN_H
+
+#include "nodes/execnodes.h"
+
+extern TidRangeScanState *ExecInitTidRangeScan(TidRangeScan *node, EState *estate, int eflags);
+extern void ExecEndTidRangeScan(TidRangeScanState *node);
+extern void ExecReScanTidRangeScan(TidRangeScanState *node);
+
+#endif							/* NODETIDRANGESCAN_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 018f50b..a998bfb 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1490,6 +1490,18 @@ typedef struct TidScanState
 	HeapTupleData tss_htup;
 } TidScanState;
 
+typedef struct TidRangeScanState
+{
+	ScanState		 ss;				/* its first field is NodeTag */
+	ExprState		*lower_expr;
+	ExprState		*upper_expr;
+	BlockNumber		 first_block;
+	OffsetNumber	 first_tuple;
+	BlockNumber		 last_block;
+	OffsetNumber	 last_tuple;
+	BlockNumber		 blocks_to_scan;
+} TidRangeScanState;
+
 /* ----------------
  *	 SubqueryScanState information
  *
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 697d3d7..e983fb8 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -59,6 +59,7 @@ typedef enum NodeTag
 	T_BitmapIndexScan,
 	T_BitmapHeapScan,
 	T_TidScan,
+	T_TidRangeScan,
 	T_SubqueryScan,
 	T_FunctionScan,
 	T_ValuesScan,
@@ -115,6 +116,7 @@ typedef enum NodeTag
 	T_BitmapIndexScanState,
 	T_BitmapHeapScanState,
 	T_TidScanState,
+	T_TidRangeScanState,
 	T_SubqueryScanState,
 	T_FunctionScanState,
 	T_TableFuncScanState,
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 96d30aa..a0165e4 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -496,6 +496,21 @@ typedef struct TidScan
 } TidScan;
 
 /* ----------------
+ *		tid range scan node
+ * ----------------
+ */
+typedef struct TidRangeScan
+{
+	Scan		scan;
+	List	   *tidquals;
+	Expr		*lower_bound;
+	Expr		*upper_bound;
+	bool		lower_strict;
+	bool		upper_strict;
+	ScanDirection direction;
+} TidRangeScan;
+
+/* ----------------
  *		subquery scan node
  *
  * SubqueryScan is for scanning the output of a sub-query in the range table.
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index cf4839d..f348c38 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1229,10 +1229,21 @@ typedef struct BitmapOrPath
  * "CTID = pseudoconstant" or "CTID = ANY(pseudoconstant_array)".
  * Note they are bare expressions, not RestrictInfos.
  */
+typedef enum
+{
+	TID_PATH_LIST,			/* tidquals is a list of CTID = ?, CTID IN (?), with OR-semantics */
+	TID_PATH_RANGE			/* tidquals is a list of CTID > ?, CTID < ?, with AND-semantics */
+} TidPathMethod;
+
 typedef struct TidPath
 {
-	Path		path;
-	List	   *tidquals;		/* qual(s) involving CTID = something */
+	Path		  path;
+	List		 *tidquals;
+	TidPathMethod method;
+	Expr		 *lower_bound;
+	Expr		 *upper_bound;
+	bool		  lower_strict;
+	bool		  upper_strict;
 	ScanDirection direction;
 } TidPath;
 
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 77ca7ff..d8f7825 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -90,7 +90,8 @@ extern void cost_bitmap_and_node(BitmapAndPath *path, PlannerInfo *root);
 extern void cost_bitmap_or_node(BitmapOrPath *path, PlannerInfo *root);
 extern void cost_bitmap_tree_node(Path *path, Cost *cost, Selectivity *selec);
 extern void cost_tidscan(Path *path, PlannerInfo *root,
-			 RelOptInfo *baserel, List *tidquals, ParamPathInfo *param_info);
+			 RelOptInfo *baserel, List *tidquals, TidPathMethod method, Expr *lower_bound, Expr *upper_bound,
+			 ParamPathInfo *param_info);
 extern void cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root,
 				  RelOptInfo *baserel, ParamPathInfo *param_info);
 extern void cost_functionscan(Path *path, PlannerInfo *root,
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index a0a88a5..32867e7 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -63,8 +63,9 @@ extern BitmapOrPath *create_bitmap_or_path(PlannerInfo *root,
 					  RelOptInfo *rel,
 					  List *bitmapquals);
 extern TidPath *create_tidscan_path(PlannerInfo *root, RelOptInfo *rel,
-					List *tidquals, List *pathkeys, ScanDirection direction,
-					Relids required_outer);
+					List *tidquals, TidPathMethod method,
+					Expr *lower_bound, Expr *upper_bound, bool lower_strict, bool upper_strict,
+					Relids required_outer, ScanDirection direction, List *pathkeys);
 extern AppendPath *create_append_path(PlannerInfo *root, RelOptInfo *rel,
 				   List *subpaths, List *partial_subpaths,
 				   Relids required_outer,
diff --git a/src/include/utils/selfuncs.h b/src/include/utils/selfuncs.h
index 95e4428..cf5c090 100644
--- a/src/include/utils/selfuncs.h
+++ b/src/include/utils/selfuncs.h
@@ -227,4 +227,7 @@ extern Selectivity scalararraysel_containment(PlannerInfo *root,
 						   Oid elemtype, bool isEquality, bool useOr,
 						   int varRelid);
 
+
+extern double tid_range_selectivity(RelOptInfo *rel, Expr *lower_qual, Expr *upper_qual);
+
 #endif							/* SELFUNCS_H */
diff --git a/src/test/regress/expected/tidrangescan.out b/src/test/regress/expected/tidrangescan.out
new file mode 100644
index 0000000..573e769
--- /dev/null
+++ b/src/test/regress/expected/tidrangescan.out
@@ -0,0 +1,229 @@
+-- tests for tidrangescans
+CREATE TABLE tidrangescan(id integer, data text);
+INSERT INTO tidrangescan SELECT i,'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' FROM generate_series(1,1000) AS s(i);
+DELETE FROM tidrangescan WHERE substring(ctid::text from ',(\d+)\)')::integer > 10 OR substring(ctid::text from '\((\d+),')::integer >= 10;;
+VACUUM tidrangescan;
+-- range scans with upper bound
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid < '(1, 0)';
+            QUERY PLAN             
+-----------------------------------
+ Tid Range Scan on tidrangescan
+   TID Cond: (ctid < '(1,0)'::tid)
+(2 rows)
+
+SELECT ctid, data FROM tidrangescan WHERE ctid < '(1, 0)';
+  ctid  |                                       data                                       
+--------+----------------------------------------------------------------------------------
+ (0,1)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,2)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,3)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,4)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,5)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,6)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,7)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,8)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,9)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,10) | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+(10 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid <= '(1, 5)';
+             QUERY PLAN             
+------------------------------------
+ Tid Range Scan on tidrangescan
+   TID Cond: (ctid <= '(1,5)'::tid)
+(2 rows)
+
+SELECT ctid, data FROM tidrangescan WHERE ctid <= '(1, 5)';
+  ctid  |                                       data                                       
+--------+----------------------------------------------------------------------------------
+ (0,1)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,2)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,3)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,4)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,5)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,6)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,7)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,8)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,9)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (0,10) | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (1,1)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (1,2)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (1,3)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (1,4)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (1,5)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+(15 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid < '(0, 0)';
+            QUERY PLAN             
+-----------------------------------
+ Tid Range Scan on tidrangescan
+   TID Cond: (ctid < '(0,0)'::tid)
+(2 rows)
+
+SELECT ctid, data FROM tidrangescan WHERE ctid < '(0, 0)';
+ ctid | data 
+------+------
+(0 rows)
+
+-- range scans with lower bound
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid > '(9, 8)';
+            QUERY PLAN             
+-----------------------------------
+ Tid Range Scan on tidrangescan
+   TID Cond: (ctid > '(9,8)'::tid)
+(2 rows)
+
+SELECT ctid, data FROM tidrangescan WHERE ctid > '(9, 8)';
+  ctid  |                                       data                                       
+--------+----------------------------------------------------------------------------------
+ (9,9)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (9,10) | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+(2 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid >= '(9, 8)';
+             QUERY PLAN             
+------------------------------------
+ Tid Range Scan on tidrangescan
+   TID Cond: (ctid >= '(9,8)'::tid)
+(2 rows)
+
+SELECT ctid, data FROM tidrangescan WHERE ctid >= '(9, 8)';
+  ctid  |                                       data                                       
+--------+----------------------------------------------------------------------------------
+ (9,8)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (9,9)  | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ (9,10) | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid >= '(100, 0)';
+              QUERY PLAN              
+--------------------------------------
+ Tid Range Scan on tidrangescan
+   TID Cond: (ctid >= '(100,0)'::tid)
+(2 rows)
+
+SELECT ctid, data FROM tidrangescan WHERE ctid >= '(100, 0)';
+ ctid | data 
+------+------
+(0 rows)
+
+-- ordering with no quals should use tid range scan
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan ORDER BY ctid ASC;
+           QUERY PLAN           
+--------------------------------
+ Tid Range Scan on tidrangescan
+(1 row)
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan ORDER BY ctid DESC;
+               QUERY PLAN                
+-----------------------------------------
+ Tid Range Scan Backward on tidrangescan
+(1 row)
+
+-- min/max
+EXPLAIN (COSTS OFF)
+SELECT MIN(ctid) FROM tidrangescan;
+                  QUERY PLAN                  
+----------------------------------------------
+ Result
+   InitPlan 1 (returns $0)
+     ->  Limit
+           ->  Tid Range Scan on tidrangescan
+                 Filter: (ctid IS NOT NULL)
+(5 rows)
+
+SELECT MIN(ctid) FROM tidrangescan;
+  min  
+-------
+ (0,1)
+(1 row)
+
+EXPLAIN (COSTS OFF)
+SELECT MAX(ctid) FROM tidrangescan;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Result
+   InitPlan 1 (returns $0)
+     ->  Limit
+           ->  Tid Range Scan Backward on tidrangescan
+                 Filter: (ctid IS NOT NULL)
+(5 rows)
+
+SELECT MAX(ctid) FROM tidrangescan;
+  max   
+--------
+ (9,10)
+(1 row)
+
+EXPLAIN (COSTS OFF)
+SELECT MIN(ctid) FROM tidrangescan WHERE ctid > '(5,0)';
+                   QUERY PLAN                    
+-------------------------------------------------
+ Result
+   InitPlan 1 (returns $0)
+     ->  Limit
+           ->  Tid Range Scan on tidrangescan
+                 TID Cond: (ctid > '(5,0)'::tid)
+                 Filter: (ctid IS NOT NULL)
+(6 rows)
+
+SELECT MIN(ctid) FROM tidrangescan WHERE ctid > '(5,0)';
+  min  
+-------
+ (5,1)
+(1 row)
+
+EXPLAIN (COSTS OFF)
+SELECT MAX(ctid) FROM tidrangescan WHERE ctid < '(5,0)';
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Result
+   InitPlan 1 (returns $0)
+     ->  Limit
+           ->  Tid Range Scan Backward on tidrangescan
+                 TID Cond: (ctid < '(5,0)'::tid)
+                 Filter: (ctid IS NOT NULL)
+(6 rows)
+
+SELECT MAX(ctid) FROM tidrangescan WHERE ctid < '(5,0)';
+  max   
+--------
+ (4,10)
+(1 row)
+
+-- empty table
+CREATE TABLE tidrangescan_empty(id integer, data text);
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan_empty WHERE ctid < '(1, 0)';
+              QUERY PLAN              
+--------------------------------------
+ Tid Range Scan on tidrangescan_empty
+   TID Cond: (ctid < '(1,0)'::tid)
+(2 rows)
+
+SELECT ctid, data FROM tidrangescan_empty WHERE ctid < '(1, 0)';
+ ctid | data 
+------+------
+(0 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan_empty WHERE ctid > '(9, 0)';
+              QUERY PLAN              
+--------------------------------------
+ Tid Range Scan on tidrangescan_empty
+   TID Cond: (ctid > '(9,0)'::tid)
+(2 rows)
+
+SELECT ctid, data FROM tidrangescan_empty WHERE ctid > '(9, 0)';
+ ctid | data 
+------+------
+(0 rows)
+
diff --git a/src/test/regress/expected/tidscan.out b/src/test/regress/expected/tidscan.out
index 7eebe77..e0ec664 100644
--- a/src/test/regress/expected/tidscan.out
+++ b/src/test/regress/expected/tidscan.out
@@ -116,6 +116,25 @@ FETCH FIRST FROM c;
 (1 row)
 
 ROLLBACK;
+-- make sure that tid scan is chosen rather than tid range scan, if there are equality/in quals as well as range quals
+EXPLAIN (COSTS OFF)
+SELECT ctid, * FROM tidscan WHERE ctid = '(0,1)' AND ctid < '(3,0)';
+            QUERY PLAN             
+-----------------------------------
+ Tid Scan on tidscan
+   TID Cond: (ctid = '(0,1)'::tid)
+   Filter: (ctid < '(3,0)'::tid)
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, * FROM tidscan WHERE ctid = ANY(ARRAY['(0,1)', '(0,2)']::tid[]) AND ctid < '(3,0)';
+                      QUERY PLAN                       
+-------------------------------------------------------
+ Tid Scan on tidscan
+   TID Cond: (ctid = ANY ('{"(0,1)","(0,2)"}'::tid[]))
+   Filter: (ctid < '(3,0)'::tid)
+(3 rows)
+
 -- check that ordering on a tidscan doesn't require a sort
 EXPLAIN (COSTS OFF)
 SELECT ctid, * FROM tidscan WHERE ctid = ANY(ARRAY['(0,2)', '(0,1)', '(0,3)']::tid[]) ORDER BY ctid;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 16f979c..517a469 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -89,7 +89,7 @@ test: brin gin gist spgist privileges init_privs security_label collate matview
 # ----------
 # Another group of parallel tests
 # ----------
-test: alter_generic alter_operator misc psql async dbsize misc_functions sysviews tsrf tidscan stats_ext
+test: alter_generic alter_operator misc psql async dbsize misc_functions sysviews tsrf tidscan tidrangescan stats_ext
 
 # rules cannot run concurrently with any test that creates a view
 test: rules psql_crosstab amutils
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 42632be..c97f1c6 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -134,6 +134,7 @@ test: misc_functions
 test: sysviews
 test: tsrf
 test: tidscan
+test: tidrangescan
 test: stats_ext
 test: rules
 test: psql_crosstab
diff --git a/src/test/regress/sql/tidrangescan.sql b/src/test/regress/sql/tidrangescan.sql
new file mode 100644
index 0000000..de132b8
--- /dev/null
+++ b/src/test/regress/sql/tidrangescan.sql
@@ -0,0 +1,68 @@
+-- tests for tidrangescans
+
+CREATE TABLE tidrangescan(id integer, data text);
+
+INSERT INTO tidrangescan SELECT i,'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' FROM generate_series(1,1000) AS s(i);
+DELETE FROM tidrangescan WHERE substring(ctid::text from ',(\d+)\)')::integer > 10 OR substring(ctid::text from '\((\d+),')::integer >= 10;;
+VACUUM tidrangescan;
+
+-- range scans with upper bound
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid < '(1, 0)';
+SELECT ctid, data FROM tidrangescan WHERE ctid < '(1, 0)';
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid <= '(1, 5)';
+SELECT ctid, data FROM tidrangescan WHERE ctid <= '(1, 5)';
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid < '(0, 0)';
+SELECT ctid, data FROM tidrangescan WHERE ctid < '(0, 0)';
+
+-- range scans with lower bound
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid > '(9, 8)';
+SELECT ctid, data FROM tidrangescan WHERE ctid > '(9, 8)';
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid >= '(9, 8)';
+SELECT ctid, data FROM tidrangescan WHERE ctid >= '(9, 8)';
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan WHERE ctid >= '(100, 0)';
+SELECT ctid, data FROM tidrangescan WHERE ctid >= '(100, 0)';
+
+-- ordering with no quals should use tid range scan
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan ORDER BY ctid ASC;
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan ORDER BY ctid DESC;
+
+-- min/max
+EXPLAIN (COSTS OFF)
+SELECT MIN(ctid) FROM tidrangescan;
+SELECT MIN(ctid) FROM tidrangescan;
+
+EXPLAIN (COSTS OFF)
+SELECT MAX(ctid) FROM tidrangescan;
+SELECT MAX(ctid) FROM tidrangescan;
+
+EXPLAIN (COSTS OFF)
+SELECT MIN(ctid) FROM tidrangescan WHERE ctid > '(5,0)';
+SELECT MIN(ctid) FROM tidrangescan WHERE ctid > '(5,0)';
+
+EXPLAIN (COSTS OFF)
+SELECT MAX(ctid) FROM tidrangescan WHERE ctid < '(5,0)';
+SELECT MAX(ctid) FROM tidrangescan WHERE ctid < '(5,0)';
+
+-- empty table
+CREATE TABLE tidrangescan_empty(id integer, data text);
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan_empty WHERE ctid < '(1, 0)';
+SELECT ctid, data FROM tidrangescan_empty WHERE ctid < '(1, 0)';
+
+EXPLAIN (COSTS OFF)
+SELECT ctid, data FROM tidrangescan_empty WHERE ctid > '(9, 0)';
+SELECT ctid, data FROM tidrangescan_empty WHERE ctid > '(9, 0)';
diff --git a/src/test/regress/sql/tidscan.sql b/src/test/regress/sql/tidscan.sql
index 5237f06..05fef3c 100644
--- a/src/test/regress/sql/tidscan.sql
+++ b/src/test/regress/sql/tidscan.sql
@@ -43,6 +43,12 @@ FETCH BACKWARD 1 FROM c;
 FETCH FIRST FROM c;
 ROLLBACK;
 
+-- make sure that tid scan is chosen rather than tid range scan, if there are equality/in quals as well as range quals
+EXPLAIN (COSTS OFF)
+SELECT ctid, * FROM tidscan WHERE ctid = '(0,1)' AND ctid < '(3,0)';
+EXPLAIN (COSTS OFF)
+SELECT ctid, * FROM tidscan WHERE ctid = ANY(ARRAY['(0,1)', '(0,2)']::tid[]) AND ctid < '(3,0)';
+
 -- check that ordering on a tidscan doesn't require a sort
 EXPLAIN (COSTS OFF)
 SELECT ctid, * FROM tidscan WHERE ctid = ANY(ARRAY['(0,2)', '(0,1)', '(0,3)']::tid[]) ORDER BY ctid;
