On 06.04.2017 01:24, Andres Freund wrote:
Unfortunately I don't think this patch has received sufficient design
and implementation to consider merging it into v10.  As code freeze is
in two days, I think we'll have to move this to the next commitfest.

We rebased our patch on top of commit 393d47ed0f5b764341c7733ef60e8442d3e9bdc2
from "Mon Jul 31 11:24:51 2017 +0900".

Best regards,
Anton, Johann, Michael, Peter
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7648201..a373358 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -919,6 +919,12 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_SeqScan:
 			pname = sname = "Seq Scan";
 			break;
+		case T_TemporalAdjustment:
+			if(((TemporalAdjustment *) plan)->temporalCl->temporalType == TEMPORAL_TYPE_ALIGNER)
+				pname = sname = "Adjustment(for ALIGN)";
+			else
+				pname = sname = "Adjustment(for NORMALIZE)";
+			break;
 		case T_SampleScan:
 			pname = sname = "Sample Scan";
 			break;
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index 083b20f..b0d6d15 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -29,6 +29,6 @@ OBJS = execAmi.o execCurrent.o execExpr.o execExprInterp.o \
        nodeCtescan.o nodeNamedtuplestorescan.o nodeWorktablescan.o \
        nodeGroup.o nodeSubplan.o nodeSubqueryscan.o nodeTidscan.o \
        nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o \
-       nodeTableFuncscan.o
+       nodeTableFuncscan.o nodeTemporalAdjustment.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/executor/execProcnode.c b/src/backend/executor/execProcnode.c
index 396920c..7dd7474 100644
--- a/src/backend/executor/execProcnode.c
+++ b/src/backend/executor/execProcnode.c
@@ -113,6 +113,7 @@
 #include "executor/nodeValuesscan.h"
 #include "executor/nodeWindowAgg.h"
 #include "executor/nodeWorktablescan.h"
+#include "executor/nodeTemporalAdjustment.h"
 #include "nodes/nodeFuncs.h"
 #include "miscadmin.h"
 
@@ -364,6 +365,11 @@ ExecInitNode(Plan *node, EState *estate, int eflags)
 												 estate, eflags);
 			break;
 
+		case T_TemporalAdjustment:
+			result = (PlanState *) ExecInitTemporalAdjustment((TemporalAdjustment *) node,
+												 estate, eflags);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
 			result = NULL;		/* keep compiler quiet */
@@ -711,6 +717,10 @@ ExecEndNode(PlanState *node)
 			ExecEndLimit((LimitState *) node);
 			break;
 
+		case T_TemporalAdjustmentState:
+			ExecEndTemporalAdjustment((TemporalAdjustmentState *) node);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
 			break;
diff --git a/src/backend/executor/nodeTemporalAdjustment.c b/src/backend/executor/nodeTemporalAdjustment.c
new file mode 100644
index 0000000..ff2aa85
--- /dev/null
+++ b/src/backend/executor/nodeTemporalAdjustment.c
@@ -0,0 +1,571 @@
+#include "postgres.h"
+#include "executor/executor.h"
+#include "executor/nodeTemporalAdjustment.h"
+#include "utils/memutils.h"
+#include "access/htup_details.h"				/* for heap_getattr */
+#include "utils/lsyscache.h"
+#include "nodes/print.h"						/* for print_slot */
+#include "utils/datum.h"						/* for datumCopy */
+#include "utils/rangetypes.h"
+
+/*
+ * #define TEMPORAL_DEBUG
+ * XXX PEMOSER Maybe we should use execdebug.h stuff here?
+ */
+#ifdef TEMPORAL_DEBUG
+static char*
+datumToString(Oid typeinfo, Datum attr)
+{
+	Oid			typoutput;
+	bool		typisvarlena;
+	getTypeOutputInfo(typeinfo, &typoutput, &typisvarlena);
+	return OidOutputFunctionCall(typoutput, attr);
+}
+
+#define TPGdebug(...) 					{ printf(__VA_ARGS__); printf("\n"); fflush(stdout); }
+#define TPGdebugDatum(attr, typeinfo) 	TPGdebug("%s = %s %ld\n", #attr, datumToString(typeinfo, attr), attr)
+#define TPGdebugSlot(slot) 				{ printf("Printing Slot '%s'\n", #slot); print_slot(slot); fflush(stdout); }
+
+#else
+#define datumToString(typeinfo, attr)
+#define TPGdebug(...)
+#define TPGdebugDatum(attr, typeinfo)
+#define TPGdebugSlot(slot)
+#endif
+
+/*
+ * isLessThan
+ *		We must check if the sweepline is before a timepoint, or if a timepoint
+ *		is smaller than another. We initialize the function call info during
+ *		ExecInit phase.
+ */
+static bool
+isLessThan(Datum a, Datum b, TemporalAdjustmentState* node)
+{
+	node->ltFuncCallInfo.arg[0] = a;
+	node->ltFuncCallInfo.arg[1] = b;
+	node->ltFuncCallInfo.argnull[0] = false;
+	node->ltFuncCallInfo.argnull[1] = false;
+
+	/* Return value is never null, due to the pre-defined sub-query output */
+	return DatumGetBool(FunctionCallInvoke(&node->ltFuncCallInfo));
+}
+
+/*
+ * isEqual
+ *		We must check if two timepoints are equal. We initialize the function
+ *		call info during ExecInit phase.
+ */
+static bool
+isEqual(Datum a, Datum b, TemporalAdjustmentState* node)
+{
+	node->eqFuncCallInfo.arg[0] = a;
+	node->eqFuncCallInfo.arg[1] = b;
+	node->eqFuncCallInfo.argnull[0] = false;
+	node->eqFuncCallInfo.argnull[1] = false;
+
+	/* Return value is never null, due to the pre-defined sub-query output */
+	return DatumGetBool(FunctionCallInvoke(&node->eqFuncCallInfo));
+}
+
+/*
+ * makeRange
+ *		We split range types into two scalar boundary values (i.e., upper and
+ *		lower bound). Due to this splitting, we can keep a single version of
+ *		the algorithm with for two separate boundaries. However, we must combine
+ *		these two scalars at the end to return the same datatypes as we got for
+ *		the input. The drawback of this approach is that we loose boundary types
+ *		here, i.e., we do not know if a bound was inclusive or exclusive. We
+ *		initialize the function call info during ExecInit phase.
+ */
+static Datum
+makeRange(Datum l, Datum u, TemporalAdjustmentState* node)
+{
+	node->rcFuncCallInfo.arg[0] = l;
+	node->rcFuncCallInfo.arg[1] = u;
+	node->rcFuncCallInfo.argnull[0] = false;
+	node->rcFuncCallInfo.argnull[1] = false;
+
+	/* Return value is never null, due to the pre-defined sub-query output */
+	return FunctionCallInvoke(&node->rcFuncCallInfo);
+}
+
+static Datum
+getLower(Datum range, TemporalAdjustmentState* node)
+{
+	node->loFuncCallInfo.arg[0] = range;
+	node->loFuncCallInfo.argnull[0] = false;
+
+	/* Return value is never null, due to the pre-defined sub-query output */
+	return FunctionCallInvoke(&node->loFuncCallInfo);
+}
+
+static Datum
+getUpper(Datum range, TemporalAdjustmentState* node)
+{
+	node->upFuncCallInfo.arg[0] = range;
+	node->upFuncCallInfo.argnull[0] = false;
+
+	/* Return value is never null, due to the pre-defined sub-query output */
+	return FunctionCallInvoke(&node->upFuncCallInfo);
+}
+
+/*
+ * temporalAdjustmentStoreTuple
+ *      While we store result tuples, we must add the newly calculated temporal
+ *      boundaries as two scalar fields or create a single range-typed field
+ *      with the two given boundaries.
+ */
+static void
+temporalAdjustmentStoreTuple(TemporalAdjustmentState* node,
+							 TupleTableSlot* slotToModify,
+							 TupleTableSlot* slotToStoreIn,
+							 Datum newTs,
+							 Datum newTe)
+{
+	MemoryContext oldContext;
+	HeapTuple t;
+
+	node->newValues[node->temporalCl->attNumTr - 1] =
+					makeRange(newTs, newTe, node);
+
+	oldContext = MemoryContextSwitchTo(node->ss.ps.ps_ResultTupleSlot->tts_mcxt);
+	t = heap_modify_tuple(slotToModify->tts_tuple,
+						  slotToModify->tts_tupleDescriptor,
+						  node->newValues,
+						  node->nullMask,
+						  node->tsteMask);
+	MemoryContextSwitchTo(oldContext);
+	slotToStoreIn = ExecStoreTuple(t, slotToStoreIn, InvalidBuffer, true);
+
+	TPGdebug("Storing tuple:");
+	TPGdebugSlot(slotToStoreIn);
+}
+
+/*
+ * slotGetAttrNotNull
+ *      Same as slot_getattr, but throws an error if NULL is returned.
+ */
+static Datum
+slotGetAttrNotNull(TupleTableSlot *slot, int attnum)
+{
+	bool isNull;
+	Datum result;
+
+	result = slot_getattr(slot, attnum, &isNull);
+
+	if(isNull)
+		ereport(ERROR,
+				(errcode(ERRCODE_NOT_NULL_VIOLATION),
+				 errmsg("Attribute \"%s\" at position %d is null. Temporal " \
+						"adjustment not possible.",
+				 NameStr(slot->tts_tupleDescriptor->attrs[attnum - 1]->attname),
+				 attnum)));
+
+	return result;
+}
+
+/*
+ * heapGetAttrNotNull
+ *      Same as heap_getattr, but throws an error if NULL is returned.
+ */
+static Datum
+heapGetAttrNotNull(TupleTableSlot *slot, int attnum)
+{
+	bool isNull;
+	Datum result;
+
+	result = heap_getattr(slot->tts_tuple,
+						  attnum,
+						  slot->tts_tupleDescriptor,
+						  &isNull);
+	if(isNull)
+		ereport(ERROR,
+				(errcode(ERRCODE_NOT_NULL_VIOLATION),
+				 errmsg("Attribute \"%s\" at position %d is null. Temporal " \
+						"adjustment not possible.",
+				 NameStr(slot->tts_tupleDescriptor->attrs[attnum - 1]->attname),
+				 attnum)));
+
+	return result;
+}
+
+#define setSweepline(datum) \
+	node->sweepline = datumCopy(datum, node->datumFormat->attbyval, node->datumFormat->attlen)
+
+#define freeSweepline() \
+	if (! node->datumFormat->attbyval) pfree(DatumGetPointer(node->sweepline))
+
+/*
+ * ExecTemporalAdjustment
+ *
+ * At this point we get an input, which is splitted into so-called temporal
+ * groups. Each of these groups satisfy the theta-condition (see below), has
+ * overlapping periods, and a row number as ID. The input is ordered by temporal
+ * group ID, and the start and ending timepoints, i.e., P1 and P2. Temporal
+ * normalizers do not make a distinction between start and end timepoints while
+ * grouping, therefore we have only one timepoint attribute there (i.e., P1),
+ * which is the union of start and end timepoints.
+ *
+ * This executor function implements both temporal primitives, namely temporal
+ * aligner and temporal normalizer. We keep a sweep line which starts from
+ * the lowest start point, and proceeds to the right. Please note, that
+ * both algorithms need a different input to work.
+ *
+ * (1) TEMPORAL ALIGNER
+ *     Temporal aligners are used to build temporal joins. The general idea of
+ *     alignment is to split each tuple of its right argument r with respect to
+ *     each tuple in the group of tuples in the left argument s that satisfies
+ *     theta, and has overlapping timestamp intervals.
+ *
+ * 	Example:
+ * 	  ... FROM (r ALIGN s ON theta WITH (r.t, s.t)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1, P2)
+ * 	  where r_1,...,r_n are all attributes from relation r. One of these
+ * 	  attributes is a range-typed valid time attribute, namely T. The interval
+ * 	  T = [TStart,TEnd) represents the VALID TIME of each tuple. RN is the
+ * 	  temporal group ID or row number, P1 is the greatest starting
+ * 	  timepoint, and P2 is the least ending timepoint of corresponding
+ * 	  temporal attributes of the relations r and s. The interval [P1,P2)
+ * 	  holds the already computed intersection between r- and s-tuples.
+ *
+ * (2) TEMPORAL NORMALIZER
+ * 	   Temporal normalizers are used to build temporal set operations,
+ * 	   temporal aggregations, and temporal projections (i.e., DISTINCT).
+ * 	   The general idea of normalization is to split each tuple in r with
+ * 	   respect to the group of tuples in s that match on the grouping
+ * 	   attributes in B (i.e., the USING clause, which can also be empty, or
+ * 	   contain more than one attribute). In addition, also non-equality
+ * 	   comparisons can be made by substituting USING with "ON theta".
+ *
+ * 	Example:
+ * 	  ... FROM (r NORMALIZE s USING(B) WITH (r.t, s.t)) x
+ * 	  or
+ * 	  ... FROM (r NORMALIZE s ON theta WITH (r.t, s.t)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1)
+ * 	  where r_1,...,r_n are all attributes from relation r. One of these
+ * 	  attributes is a range-typed valid time attribute, namely T. The interval
+ * 	  T = [TStart,TEnd) represents the VALID TIME of each tuple. RN is the
+ * 	  temporal group ID or row number, and P1 is union of both
+ * 	  timepoints TStart and TEnd of relation s.
+ */
+TupleTableSlot *
+ExecTemporalAdjustment(TemporalAdjustmentState *node)
+{
+	PlanState  			*outerPlan 	= outerPlanState(node);
+	TupleTableSlot 		*out		= node->ss.ps.ps_ResultTupleSlot;
+	TupleTableSlot 		*curr		= outerPlan->ps_ResultTupleSlot;
+	TupleTableSlot 		*prev 		= node->ss.ss_ScanTupleSlot;
+	TemporalClause		*tc 		= node->temporalCl;
+	bool 				 produced;
+	bool 				 isNull;
+	Datum 				 currP1;	/* Current tuple's P1 */
+	Datum 				 currP2;	/* Current tuple's P2 (ALIGN only) */
+	Datum				 currRN;	/* Current tuple's row number */
+	Datum				 prevRN;	/* Previous tuple's row number */
+	Datum				 prevTe;	/* Previous tuple's time end point*/
+
+	if(node->firstCall)
+	{
+		curr = ExecProcNode(outerPlan);
+		if(TupIsNull(curr))
+			return NULL;
+
+		prev = ExecCopySlot(prev, curr);
+		node->sameleft = true;
+		node->firstCall = false;
+		node->outrn = 0;
+
+		/*
+		 * P1 is made of the lower or upper bounds of the valid time column,
+		 * hence it must have the same type as the range (return element type)
+		 * of lower(T) or upper(T).
+		 */
+		node->datumFormat = curr->tts_tupleDescriptor->attrs[tc->attNumP1 - 1];
+		setSweepline(getLower(slotGetAttrNotNull(curr, tc->attNumTr), node));
+	}
+
+	TPGdebugSlot(curr);
+	TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+	TPGdebug("node->sameleft = %d", node->sameleft);
+
+	produced = false;
+	while(!produced && !TupIsNull(prev))
+	{
+		if(node->sameleft)
+		{
+			currRN = slotGetAttrNotNull(curr, tc->attNumRN);
+
+			/*
+			 * The right-hand-side of the LEFT OUTER JOIN can produce
+			 * null-values, however we must produce a result tuple anyway with
+			 * the attributes of the left-hand-side, if this happens.
+			 */
+			currP1 = slot_getattr(curr,  tc->attNumP1, &isNull);
+			if (isNull)
+				node->sameleft = false;
+
+			if(!isNull && isLessThan(node->sweepline, currP1, node))
+			{
+				temporalAdjustmentStoreTuple(node, curr, out,
+								node->sweepline, currP1);
+				produced = true;
+				freeSweepline();
+				setSweepline(currP1);
+				node->outrn = DatumGetInt64(currRN);
+			}
+			else
+			{
+				/*
+				 * Temporal aligner: currP1/2 can never be NULL, therefore we
+				 * never enter this block. We do not have to check for currP1/2
+				 * equal NULL.
+				 */
+				if(node->alignment)
+				{
+					/* We fetched currP1 and currRN already */
+					currP2 = slotGetAttrNotNull(curr, tc->attNumP2);
+
+					/* If alignment check to not produce the same tuple again */
+					if(TupIsNull(out)
+						|| !isEqual(getLower(heapGetAttrNotNull(out, tc->attNumTr),
+											 node),
+									currP1, node)
+						|| !isEqual(getUpper(heapGetAttrNotNull(out, tc->attNumTr),
+											 node),
+									currP2, node)
+						|| node->outrn != DatumGetInt64(currRN))
+					{
+						temporalAdjustmentStoreTuple(node, curr, out,
+													 currP1, currP2);
+
+						/* sweepline = max(sweepline, curr.P2) */
+						if (isLessThan(node->sweepline, currP2, node))
+						{
+							freeSweepline();
+							setSweepline(currP2);
+						}
+
+						node->outrn = DatumGetInt64(currRN);
+						produced = true;
+					}
+				}
+
+				prev = ExecCopySlot(prev, curr);
+				curr = ExecProcNode(outerPlan);
+
+				if(TupIsNull(curr))
+					node->sameleft = false;
+				else
+				{
+					currRN = slotGetAttrNotNull(curr, tc->attNumRN);
+					prevRN = slotGetAttrNotNull(prev, tc->attNumRN);
+					node->sameleft =
+							DatumGetInt64(currRN) == DatumGetInt64(prevRN);
+				}
+			}
+		}
+		else
+		{
+			prevTe = getUpper(heapGetAttrNotNull(prev, tc->attNumTr), node);
+
+			if(isLessThan(node->sweepline, prevTe, node))
+			{
+				temporalAdjustmentStoreTuple(node, prev, out,
+								node->sweepline, prevTe);
+
+				/*
+				 * We fetch the row number from the previous tuple slot,
+				 * since it is possible that the current one is NULL, if we
+				 * arrive here from sameleft = false set when curr = NULL.
+				 */
+				currRN = heapGetAttrNotNull(prev, tc->attNumRN);
+				node->outrn = DatumGetInt64(currRN);
+				produced = true;
+			}
+
+			if(TupIsNull(curr))
+				prev = ExecClearTuple(prev);
+			else
+			{
+				prev = ExecCopySlot(prev, curr);
+				freeSweepline();
+				setSweepline(getLower(slotGetAttrNotNull(curr, tc->attNumTr),
+									  node));
+			}
+			node->sameleft = true;
+		}
+	}
+
+	if(!produced) {
+		ExecClearTuple(out);
+		return NULL;
+	}
+
+	return out;
+}
+
+/*
+ * ExecInitTemporalAdjustment
+ * 		Initializes the tuple memory context, outer plan node, and function call
+ * 		infos for makeRange, lessThan, and isEqual including collation types.
+ * 		A range constructor function is only initialized if temporal boundaries
+ * 		are given as range types.
+ */
+TemporalAdjustmentState *
+ExecInitTemporalAdjustment(TemporalAdjustment *node, EState *estate, int eflags)
+{
+	TemporalAdjustmentState *state;
+	FmgrInfo		 		*eqFunctionInfo = palloc(sizeof(FmgrInfo));
+	FmgrInfo		 		*ltFunctionInfo = palloc(sizeof(FmgrInfo));
+	FmgrInfo		 		*rcFunctionInfo = palloc(sizeof(FmgrInfo));
+	FmgrInfo		 		*loFunctionInfo = palloc(sizeof(FmgrInfo));
+	FmgrInfo		 		*upFunctionInfo = palloc(sizeof(FmgrInfo));
+
+	/* check for unsupported flags */
+	Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
+
+	/*
+	 * create state structure
+	 */
+	state = makeNode(TemporalAdjustmentState);
+	state->ss.ps.plan = (Plan *) node;
+	state->ss.ps.state = estate;
+	state->ss.ps.ExecProcNode = ExecTemporalAdjustment;
+
+	/*
+	 * Miscellaneous initialization
+	 *
+	 * Temporal Adjustment nodes have no ExprContext initialization because
+	 * they never call ExecQual or ExecProject.  But they do need a per-tuple
+	 * memory context anyway for calling execTuplesMatch.
+	 * XXX PEMOSER Do we need this really?
+	 */
+	state->tempContext =
+		AllocSetContextCreate(CurrentMemoryContext,
+							  "TemporalAdjustment",
+							  ALLOCSET_DEFAULT_MINSIZE,
+							  ALLOCSET_DEFAULT_INITSIZE,
+							  ALLOCSET_DEFAULT_MAXSIZE);
+
+	/*
+	 * Tuple table initialization
+	 */
+	ExecInitResultTupleSlot(estate, &state->ss.ps);
+	ExecInitScanTupleSlot(estate, &state->ss);
+
+	/*
+	 * then initialize outer plan
+	 */
+	outerPlanState(state) = ExecInitNode(outerPlan(node), estate, eflags);
+
+	/*
+	* initialize source tuple type.
+	*/
+	ExecAssignScanTypeFromOuterPlan(&state->ss);
+
+	/*
+	 * Temporal adjustment nodes do no projections, so initialize projection
+	 * info for this node appropriately
+	 */
+	ExecAssignResultTypeFromTL(&state->ss.ps);
+	state->ss.ps.ps_ProjInfo = NULL;
+
+	state->alignment = node->temporalCl->temporalType == TEMPORAL_TYPE_ALIGNER;
+	state->temporalCl = copyObject(node->temporalCl);
+	state->firstCall = true;
+	state->sweepline = (Datum) 0;
+
+	/*
+	 * Init masks
+	 */
+	state->nullMask = palloc0(sizeof(bool) * node->numCols);
+	state->tsteMask = palloc0(sizeof(bool) * node->numCols);
+	state->tsteMask[state->temporalCl->attNumTr - 1] = true;
+
+	/*
+	 * Init buffer values for heap_modify_tuple
+	 */
+	state->newValues = palloc0(sizeof(Datum) * node->numCols);
+
+	/* the parser should have made sure of this */
+	Assert(OidIsValid(node->ltOperatorID));
+	Assert(OidIsValid(node->eqOperatorID));
+
+	/*
+	 * Precompute fmgr lookup data for inner loop. We use "less than", "equal",
+	 * and "range_constructor2" operators on columns with indexes "tspos",
+	 * "tepos", and "trpos" respectively. To construct a range type we also
+	 * assign the original range information from the targetlist entry which
+	 * holds the range type from the input to the function call info expression.
+	 * This expression is then used to determine the correct type and collation.
+	 */
+	fmgr_info(get_opcode(node->eqOperatorID), eqFunctionInfo);
+	fmgr_info(get_opcode(node->ltOperatorID), ltFunctionInfo);
+
+	InitFunctionCallInfoData(state->eqFuncCallInfo, eqFunctionInfo, 2,
+							 node->sortCollationID, NULL, NULL);
+	InitFunctionCallInfoData(state->ltFuncCallInfo, ltFunctionInfo, 2,
+							 node->sortCollationID, NULL, NULL);
+
+	/*
+	 * Prepare function manager information to extract lower and upper bounds
+	 * of range types, and to call the range constructor method to build a new
+	 * range type out of two separate boundaries.
+	 */
+	fmgr_info(fmgr_internal_function("range_constructor2"), rcFunctionInfo);
+	rcFunctionInfo->fn_expr = (fmNodePtr) node->rangeVar;
+	InitFunctionCallInfoData(state->rcFuncCallInfo, rcFunctionInfo, 2,
+							 node->rangeVar->varcollid, NULL, NULL);
+
+	fmgr_info(fmgr_internal_function("range_lower"), loFunctionInfo);
+	loFunctionInfo->fn_expr = (fmNodePtr) node->rangeVar;
+	InitFunctionCallInfoData(state->loFuncCallInfo, loFunctionInfo, 1,
+							 node->rangeVar->varcollid, NULL, NULL);
+
+	fmgr_info(fmgr_internal_function("range_upper"), upFunctionInfo);
+	upFunctionInfo->fn_expr = (fmNodePtr) node->rangeVar;
+	InitFunctionCallInfoData(state->upFuncCallInfo, upFunctionInfo, 1,
+							 node->rangeVar->varcollid, NULL, NULL);
+
+#ifdef TEMPORAL_DEBUG
+	printf("TEMPORAL ADJUSTMENT EXECUTOR INIT...\n");
+	pprint(node->temporalCl);
+	fflush(stdout);
+#endif
+
+	return state;
+}
+
+void
+ExecEndTemporalAdjustment(TemporalAdjustmentState *node)
+{
+	/* clean up tuple table */
+	ExecClearTuple(node->ss.ps.ps_ResultTupleSlot);
+	ExecClearTuple(node->ss.ss_ScanTupleSlot);
+
+	MemoryContextDelete(node->tempContext);
+
+	/* shut down the subplans */
+	ExecEndNode(outerPlanState(node));
+}
+
+
+/*
+ * XXX PEMOSER Is an ExecReScan needed for NORMALIZE/ALIGN?
+ */
+void
+ExecReScanTemporalAdjustment(TemporalAdjustmentState *node)
+{
+	/* must clear result tuple so first input tuple is returned */
+	ExecClearTuple(node->ss.ps.ps_ResultTupleSlot);
+
+	/*
+	 * if chgParam of subnode is not null then plan will be re-scanned by
+	 * first ExecProcNode.
+	 */
+	if (node->ss.ps.lefttree->chgParam == NULL)
+		ExecReScan(node->ss.ps.lefttree);
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 45a04b0..9935c3d 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2083,6 +2083,8 @@ _copyJoinExpr(const JoinExpr *from)
 	COPY_NODE_FIELD(quals);
 	COPY_NODE_FIELD(alias);
 	COPY_SCALAR_FIELD(rtindex);
+	COPY_NODE_FIELD(temporalBounds);
+	COPY_SCALAR_FIELD(inTmpPrimTempType);
 
 	return newnode;
 }
@@ -2461,6 +2463,41 @@ _copyOnConflictClause(const OnConflictClause *from)
 	return newnode;
 }
 
+static TemporalClause *
+_copyTemporalClause(const TemporalClause *from)
+{
+	TemporalClause *newnode = makeNode(TemporalClause);
+
+	COPY_SCALAR_FIELD(temporalType);
+	COPY_SCALAR_FIELD(attNumTr);
+	COPY_SCALAR_FIELD(attNumP1);
+	COPY_SCALAR_FIELD(attNumP2);
+	COPY_SCALAR_FIELD(attNumRN);
+	COPY_STRING_FIELD(colnameTr);
+
+	return newnode;
+}
+
+static TemporalAdjustment *
+_copyTemporalAdjustment(const TemporalAdjustment *from)
+{
+	TemporalAdjustment *newnode = makeNode(TemporalAdjustment);
+
+	/*
+	 * copy node superclass fields
+	 */
+	CopyPlanFields((const Plan *) from, (Plan *) newnode);
+
+	COPY_SCALAR_FIELD(numCols);
+	COPY_SCALAR_FIELD(eqOperatorID);
+	COPY_SCALAR_FIELD(ltOperatorID);
+	COPY_SCALAR_FIELD(sortCollationID);
+	COPY_NODE_FIELD(temporalCl);
+	COPY_NODE_FIELD(rangeVar);
+
+	return newnode;
+}
+
 static CommonTableExpr *
 _copyCommonTableExpr(const CommonTableExpr *from)
 {
@@ -2956,6 +2993,7 @@ _copyQuery(const Query *from)
 	COPY_NODE_FIELD(setOperations);
 	COPY_NODE_FIELD(constraintDeps);
 	COPY_NODE_FIELD(withCheckOptions);
+	COPY_NODE_FIELD(temporalClause);
 	COPY_LOCATION_FIELD(stmt_location);
 	COPY_LOCATION_FIELD(stmt_len);
 
@@ -3038,6 +3076,7 @@ _copySelectStmt(const SelectStmt *from)
 	COPY_NODE_FIELD(limitCount);
 	COPY_NODE_FIELD(lockingClause);
 	COPY_NODE_FIELD(withClause);
+	COPY_NODE_FIELD(temporalClause);
 	COPY_SCALAR_FIELD(op);
 	COPY_SCALAR_FIELD(all);
 	COPY_NODE_FIELD(larg);
@@ -5507,6 +5546,12 @@ copyObjectImpl(const void *from)
 		case T_RoleSpec:
 			retval = _copyRoleSpec(from);
 			break;
+		case T_TemporalClause:
+			retval = _copyTemporalClause(from);
+			break;
+		case T_TemporalAdjustment:
+			retval = _copyTemporalAdjustment(from);
+			break;
 		case T_TriggerTransition:
 			retval = _copyTriggerTransition(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 8d92c03..5ac731e 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -785,6 +785,8 @@ _equalJoinExpr(const JoinExpr *a, const JoinExpr *b)
 	COMPARE_NODE_FIELD(quals);
 	COMPARE_NODE_FIELD(alias);
 	COMPARE_SCALAR_FIELD(rtindex);
+	COMPARE_NODE_FIELD(temporalBounds);
+	COMPARE_SCALAR_FIELD(inTmpPrimTempType);
 
 	return true;
 }
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 0755039..928a5c7 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -22,6 +22,89 @@
 #include "nodes/nodeFuncs.h"
 #include "utils/lsyscache.h"
 
+/*
+ * makeColumnRef1 -
+ *		makes an ColumnRef node with a single element field-list
+ */
+ColumnRef *
+makeColumnRef1(Node *field1)
+{
+	ColumnRef 	*ref;
+
+	ref = makeNode(ColumnRef);
+	ref->fields = list_make1(field1);
+	ref->location = -1; /* Unknown location */
+
+	return ref;
+}
+
+/*
+ * makeColumnRef2 -
+ *		makes an ColumnRef node with a two elements field-list
+ */
+ColumnRef *
+makeColumnRef2(Node *field1, Node *field2)
+{
+	ColumnRef 	*ref;
+
+	ref = makeNode(ColumnRef);
+	ref->fields = list_make2(field1, field2);
+	ref->location = -1; /* Unknown location */
+
+	return ref;
+}
+
+/*
+ * makeResTarget -
+ *		makes an ResTarget node
+ */
+ResTarget *
+makeResTarget(Node *val, char *name)
+{
+	ResTarget *rt;
+
+	rt = makeNode(ResTarget);
+	rt->location = -1; /* unknown location */
+	rt->indirection = NIL;
+	rt->name = name;
+	rt->val = val;
+
+	return rt;
+}
+
+/*
+ * makeAliasFromArgument -
+ *		Selects and returns an arguments' alias, if any. Or creates a new one
+ *		from a given RangeVar relation name.
+ */
+Alias *
+makeAliasFromArgument(Node *arg)
+{
+	Alias *alias = NULL;
+
+	/* Find aliases of arguments */
+	switch(nodeTag(arg))
+	{
+		case T_RangeSubselect:
+			alias = ((RangeSubselect *) arg)->alias;
+			break;
+		case T_RangeVar:
+		{
+			RangeVar *v = (RangeVar *) arg;
+			if (v->alias != NULL)
+				alias = v->alias;
+			else
+				alias = makeAlias(v->relname, NIL);
+			break;
+		}
+		default:
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("Argument has no alias or is not supported.")));
+	}
+
+	return alias;
+}
 
 /*
  * makeA_Expr -
@@ -611,3 +694,4 @@ makeGroupingSet(GroupingSetKind kind, List *content, int location)
 	n->location = location;
 	return n;
 }
+
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 379d92a..7ceaf57 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1664,6 +1664,8 @@ _outJoinExpr(StringInfo str, const JoinExpr *node)
 	WRITE_NODE_FIELD(quals);
 	WRITE_NODE_FIELD(alias);
 	WRITE_INT_FIELD(rtindex);
+	WRITE_NODE_FIELD(temporalBounds);
+	WRITE_ENUM_FIELD(inTmpPrimTempType, TemporalType);
 }
 
 static void
@@ -1946,6 +1948,18 @@ _outSortPath(StringInfo str, const SortPath *node)
 }
 
 static void
+_outTemporalAdjustmentPath(StringInfo str, const TemporalAdjustmentPath *node)
+{
+	WRITE_NODE_TYPE("TEMPORALADJUSTMENTPATH");
+
+	_outPathInfo(str, (const Path *) node);
+
+	WRITE_NODE_FIELD(subpath);
+	WRITE_NODE_FIELD(sortClause);
+	WRITE_NODE_FIELD(temporalClause);
+}
+
+static void
 _outGroupPath(StringInfo str, const GroupPath *node)
 {
 	WRITE_NODE_TYPE("GROUPPATH");
@@ -2708,6 +2722,7 @@ _outSelectStmt(StringInfo str, const SelectStmt *node)
 	WRITE_NODE_FIELD(limitCount);
 	WRITE_NODE_FIELD(lockingClause);
 	WRITE_NODE_FIELD(withClause);
+	WRITE_NODE_FIELD(temporalClause);
 	WRITE_ENUM_FIELD(op, SetOperation);
 	WRITE_BOOL_FIELD(all);
 	WRITE_NODE_FIELD(larg);
@@ -2784,6 +2799,34 @@ _outTriggerTransition(StringInfo str, const TriggerTransition *node)
 }
 
 static void
+_outTemporalAdjustment(StringInfo str, const TemporalAdjustment *node)
+{
+	WRITE_NODE_TYPE("TEMPORALADJUSTMENT");
+
+	WRITE_INT_FIELD(numCols);
+	WRITE_OID_FIELD(eqOperatorID);
+	WRITE_OID_FIELD(ltOperatorID);
+	WRITE_OID_FIELD(sortCollationID);
+	WRITE_NODE_FIELD(temporalCl);
+	WRITE_NODE_FIELD(rangeVar);
+
+	_outPlanInfo(str, (const Plan *) node);
+}
+
+static void
+_outTemporalClause(StringInfo str, const TemporalClause *node)
+{
+	WRITE_NODE_TYPE("TEMPORALCLAUSE");
+
+	WRITE_ENUM_FIELD(temporalType, TemporalType);
+	WRITE_INT_FIELD(attNumTr);
+	WRITE_INT_FIELD(attNumP1);
+	WRITE_INT_FIELD(attNumP2);
+	WRITE_INT_FIELD(attNumRN);
+	WRITE_STRING_FIELD(colnameTr);
+}
+
+static void
 _outColumnDef(StringInfo str, const ColumnDef *node)
 {
 	WRITE_NODE_TYPE("COLUMNDEF");
@@ -2918,6 +2961,7 @@ _outQuery(StringInfo str, const Query *node)
 	WRITE_NODE_FIELD(rowMarks);
 	WRITE_NODE_FIELD(setOperations);
 	WRITE_NODE_FIELD(constraintDeps);
+	WRITE_NODE_FIELD(temporalClause);
 	/* withCheckOptions intentionally omitted, see comment in parsenodes.h */
 	WRITE_LOCATION_FIELD(stmt_location);
 	WRITE_LOCATION_FIELD(stmt_len);
@@ -3956,6 +4000,9 @@ outNode(StringInfo str, const void *obj)
 			case T_SortPath:
 				_outSortPath(str, obj);
 				break;
+			case T_TemporalAdjustmentPath:
+				_outTemporalAdjustmentPath(str, obj);
+				break;
 			case T_GroupPath:
 				_outGroupPath(str, obj);
 				break;
@@ -4067,6 +4114,12 @@ outNode(StringInfo str, const void *obj)
 			case T_ExtensibleNode:
 				_outExtensibleNode(str, obj);
 				break;
+			case T_TemporalAdjustment:
+				_outTemporalAdjustment(str, obj);
+				break;
+			case T_TemporalClause:
+				_outTemporalClause(str, obj);
+				break;
 			case T_CreateStmt:
 				_outCreateStmt(str, obj);
 				break;
diff --git a/src/backend/nodes/print.c b/src/backend/nodes/print.c
index 380e8b7..cfd998c 100644
--- a/src/backend/nodes/print.c
+++ b/src/backend/nodes/print.c
@@ -25,6 +25,7 @@
 #include "optimizer/clauses.h"
 #include "parser/parsetree.h"
 #include "utils/lsyscache.h"
+#include "parser/parse_node.h"
 
 
 /*
@@ -500,3 +501,27 @@ print_slot(TupleTableSlot *slot)
 
 	debugtup(slot, NULL);
 }
+
+/*
+ * print_namespace
+ * 		print out all name space items' RTEs.
+ */
+void
+print_namespace(const List *namespace)
+{
+	ListCell   *lc;
+
+	if (list_length(namespace) == 0)
+	{
+		printf("No namespaces in list.\n");
+		fflush(stdout);
+		return;
+	}
+
+	foreach(lc, namespace)
+	{
+		ParseNamespaceItem *nsitem = (ParseNamespaceItem *) lfirst(lc);
+		pprint(nsitem->p_rte);
+	}
+
+}
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 86c811d..bbd88a1 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -262,6 +262,7 @@ _readQuery(void)
 	READ_NODE_FIELD(rowMarks);
 	READ_NODE_FIELD(setOperations);
 	READ_NODE_FIELD(constraintDeps);
+	READ_NODE_FIELD(temporalClause);
 	/* withCheckOptions intentionally omitted, see comment in parsenodes.h */
 	READ_LOCATION_FIELD(stmt_location);
 	READ_LOCATION_FIELD(stmt_len);
@@ -426,6 +427,24 @@ _readSetOperationStmt(void)
 	READ_DONE();
 }
 
+/*
+ * _readTemporalClause
+ */
+static TemporalClause *
+_readTemporalClause(void)
+{
+	READ_LOCALS(TemporalClause);
+
+	READ_ENUM_FIELD(temporalType, TemporalType);
+	READ_INT_FIELD(attNumTr);
+	READ_INT_FIELD(attNumP1);
+	READ_INT_FIELD(attNumP2);
+	READ_INT_FIELD(attNumRN);
+	READ_STRING_FIELD(colnameTr);
+
+	READ_DONE();
+}
+
 
 /*
  *	Stuff from primnodes.h.
@@ -459,6 +478,17 @@ _readRangeVar(void)
 	READ_DONE();
 }
 
+static ColumnRef *
+_readColumnRef(void)
+{
+	READ_LOCALS(ColumnRef);
+
+	READ_NODE_FIELD(fields);
+	READ_LOCATION_FIELD(location);
+
+	READ_DONE();
+}
+
 /*
  * _readTableFunc
  */
@@ -1279,6 +1309,8 @@ _readJoinExpr(void)
 	READ_NODE_FIELD(quals);
 	READ_NODE_FIELD(alias);
 	READ_INT_FIELD(rtindex);
+	READ_NODE_FIELD(temporalBounds);
+	READ_ENUM_FIELD(inTmpPrimTempType, TemporalType);
 
 	READ_DONE();
 }
@@ -2557,6 +2589,10 @@ parseNodeString(void)
 		return_value = _readDefElem();
 	else if (MATCH("DECLARECURSOR", 13))
 		return_value = _readDeclareCursorStmt();
+	else if (MATCH("TEMPORALCLAUSE", 14))
+		return_value = _readTemporalClause();
+	else if (MATCH("COLUMNREF", 9))
+		return_value = _readColumnRef();
 	else if (MATCH("PLANNEDSTMT", 11))
 		return_value = _readPlannedStmt();
 	else if (MATCH("PLAN", 4))
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index f087ddb..ae7f38d 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -44,6 +44,7 @@
 #include "parser/parsetree.h"
 #include "rewrite/rewriteManip.h"
 #include "utils/lsyscache.h"
+#include "utils/fmgroids.h"
 
 
 /* results of subquery_is_pushdown_safe */
@@ -134,7 +135,7 @@ static void recurse_push_qual(Node *setOp, Query *topquery,
 static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
 static void add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 						List *live_childrels);
-
+static bool allWindowFuncsHaveRowId(List *targetList);
 
 /*
  * make_one_rel
@@ -2508,7 +2509,8 @@ subquery_is_pushdown_safe(Query *subquery, Query *topquery,
 	/* Check points 3, 4, and 5 */
 	if (subquery->distinctClause ||
 		subquery->hasWindowFuncs ||
-		subquery->hasTargetSRFs)
+		subquery->hasTargetSRFs ||
+		subquery->temporalClause)
 		safetyInfo->unsafeVolatile = true;
 
 	/*
@@ -2618,6 +2620,7 @@ static void
 check_output_expressions(Query *subquery, pushdown_safety_info *safetyInfo)
 {
 	ListCell   *lc;
+	bool wfsafe = allWindowFuncsHaveRowId(subquery->targetList);
 
 	foreach(lc, subquery->targetList)
 	{
@@ -2656,12 +2659,29 @@ check_output_expressions(Query *subquery, pushdown_safety_info *safetyInfo)
 
 		/* If subquery uses window functions, check point 4 */
 		if (subquery->hasWindowFuncs &&
-			!targetIsInAllPartitionLists(tle, subquery))
+			!targetIsInAllPartitionLists(tle, subquery) &&
+			!wfsafe)
 		{
 			/* not present in all PARTITION BY clauses, so mark it unsafe */
 			safetyInfo->unsafeColumns[tle->resno] = true;
 			continue;
 		}
+
+		/*
+		 * If subquery uses temporal primitives, mark all columns that are
+		 * used as temporal attributes as unsafe, since they may be changed.
+		 */
+		if (subquery->temporalClause)
+		{
+			AttrNumber resnoRangeT =
+					((TemporalClause *)subquery->temporalClause)->attNumTr;
+
+			if (tle->resno == resnoRangeT)
+			{
+				safetyInfo->unsafeColumns[tle->resno] = true;
+				continue;
+			}
+		}
 	}
 }
 
@@ -2731,6 +2751,32 @@ targetIsInAllPartitionLists(TargetEntry *tle, Query *query)
 }
 
 /*
+ * allWindowFuncsHaveRowId
+ *  	True if all window functions are row_id(), otherwise false. We use this
+ *  	to have unique numbers for each tuple. It is push-down-safe, because we
+ *  	accept gaps between numbers.
+ */
+static bool
+allWindowFuncsHaveRowId(List *targetList)
+{
+	ListCell *lc;
+
+	foreach(lc, targetList)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+		if (tle->resjunk)
+			continue;
+
+		if(IsA(tle->expr, WindowFunc)
+				&& ((WindowFunc *) tle->expr)->winfnoid != F_WINDOW_ROW_ID)
+				return false;
+	}
+
+	return true;
+}
+
+/*
  * qual_is_pushdown_safe - is a particular qual safe to push down?
  *
  * qual is a restriction clause applying to the given subquery (whose RTE
@@ -2974,6 +3020,13 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel)
 		return;
 
 	/*
+	 * If there's a sub-query belonging to a temporal primitive, do not remove
+	 * any entries, because we need all of them.
+	 */
+	if (subquery->temporalClause)
+		return;
+
+	/*
 	 * Run through the tlist and zap entries we don't need.  It's okay to
 	 * modify the tlist items in-place because set_subquery_pathlist made a
 	 * copy of the subquery.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 5c934f2..9d7cfee 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -114,6 +114,9 @@ static LockRows *create_lockrows_plan(PlannerInfo *root, LockRowsPath *best_path
 static ModifyTable *create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path);
 static Limit *create_limit_plan(PlannerInfo *root, LimitPath *best_path,
 				  int flags);
+static TemporalAdjustment *create_temporaladjustment_plan(PlannerInfo *root,
+							   TemporalAdjustmentPath *best_path,
+							   int flags);
 static SeqScan *create_seqscan_plan(PlannerInfo *root, Path *best_path,
 					List *tlist, List *scan_clauses);
 static SampleScan *create_samplescan_plan(PlannerInfo *root, Path *best_path,
@@ -282,7 +285,9 @@ static ModifyTable *make_modifytable(PlannerInfo *root,
 				 List *rowMarks, OnConflictExpr *onconflict, int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
 						 GatherMergePath *best_path);
-
+static TemporalAdjustment *make_temporalAdjustment(Plan *lefttree,
+						TemporalClause *temporalClause,
+						List *sortClause);
 
 /*
  * create_plan
@@ -485,6 +490,11 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
 			plan = (Plan *) create_gather_merge_plan(root,
 													 (GatherMergePath *) best_path);
 			break;
+		case T_TemporalAdjustment:
+			plan = (Plan *) create_temporaladjustment_plan(root,
+										(TemporalAdjustmentPath *) best_path,
+										flags);
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d",
 				 (int) best_path->pathtype);
@@ -2394,6 +2404,33 @@ create_limit_plan(PlannerInfo *root, LimitPath *best_path, int flags)
 	return plan;
 }
 
+/*
+ * create_temporaladjustment_plan
+ *
+ *	  Create a Temporal Adjustment plan for 'best_path' and (recursively) plans
+ *	  for its subpaths. Depending on the type of the temporal clause, we create
+ *	  a temporal normalize or a temporal aligner node.
+ */
+static TemporalAdjustment *
+create_temporaladjustment_plan(PlannerInfo *root,
+							   TemporalAdjustmentPath *best_path,
+							   int flags)
+{
+	TemporalAdjustment	*plan;
+	Plan	   			*subplan;
+
+	/* Limit doesn't project, so tlist requirements pass through */
+	subplan = create_plan_recurse(root, best_path->subpath, flags);
+
+	plan = make_temporalAdjustment(subplan,
+								   best_path->temporalClause,
+								   best_path->sortClause);
+
+	copy_generic_path_info(&plan->plan, (Path *) best_path);
+
+	return plan;
+}
+
 
 /*****************************************************************************
  *
@@ -5116,6 +5153,57 @@ make_subqueryscan(List *qptlist,
 	return node;
 }
 
+static TemporalAdjustment *
+make_temporalAdjustment(Plan *lefttree,
+						TemporalClause *temporalClause,
+						List *sortClause)
+{
+	TemporalAdjustment 	*node = makeNode(TemporalAdjustment);
+	Plan				*plan = &node->plan;
+	SortGroupClause     *sgc;
+	TargetEntry 		*tle;
+
+	plan->targetlist = lefttree->targetlist;
+	plan->qual = NIL;
+	plan->lefttree = lefttree;
+	plan->righttree = NULL;
+
+	node->numCols = list_length(lefttree->targetlist);
+	node->temporalCl = copyObject(temporalClause);
+
+	/*
+	 * Fetch the targetlist entry of the given range type, s.t. we have all
+	 * needed information to call range_constructor inside the executor.
+	 */
+	node->rangeVar = NULL;
+	if (temporalClause->attNumTr != -1)
+	{
+		TargetEntry *tle = get_tle_by_resno(plan->targetlist,
+											temporalClause->attNumTr);
+		node->rangeVar = (Var *) copyObject(tle->expr);
+	}
+
+	/*
+	 * The last element in the sort clause is one of the temporal attributes
+	 * P1 or P2, which have the same attribute type as the valid timestamps of
+	 * both relations. Hence, we can fetch equality, sort operator, and
+	 * collation Oids from them.
+	 */
+	sgc = (SortGroupClause *) llast(sortClause);
+
+	/* the parser should have made sure of this */
+	Assert(OidIsValid(sgc->sortop));
+	Assert(OidIsValid(sgc->eqop));
+
+	node->eqOperatorID = sgc->eqop;
+	node->ltOperatorID = sgc->sortop;
+
+	tle = get_sortgroupclause_tle(sgc, plan->targetlist);
+	node->sortCollationID = exprCollation((Node *) tle->expr);
+
+	return node;
+}
+
 static FunctionScan *
 make_functionscan(List *qptlist,
 				  List *qpqual,
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 2988c11..784e263 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1968,6 +1968,20 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 		Path	   *path = (Path *) lfirst(lc);
 
 		/*
+		 * If there is a NORMALIZE or ALIGN clause, i.e., temporal primitive,
+		 * add the TemporalAdjustment node with type TemporalAligner or
+		 * TemporalNormalizer.
+		 */
+		if (parse->temporalClause)
+		{
+			path = (Path *) create_temporaladjustment_path(root,
+														 final_rel,
+														 path,
+														 parse->sortClause,
+									   (TemporalClause *)parse->temporalClause);
+		}
+
+		/*
 		 * If there is a FOR [KEY] UPDATE/SHARE clause, add the LockRows node.
 		 * (Note: we intentionally test parse->rowMarks not root->rowMarks
 		 * here.  If there are only non-locking rowmarks, they should be
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index b0c9e94..d8a758b 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -636,6 +636,7 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 		case T_Sort:
 		case T_Unique:
 		case T_SetOp:
+		case T_TemporalAdjustment:
 
 			/*
 			 * These plan types don't actually bother to evaluate their
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index ffbd3ee..f9bcbaf 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -2706,6 +2706,7 @@ finalize_plan(PlannerInfo *root, Plan *plan, Bitmapset *valid_params,
 		case T_GatherMerge:
 		case T_SetOp:
 		case T_Group:
+		case T_TemporalAdjustment:
 			/* no node-type-specific fields need fixing */
 			break;
 
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index f2d6385..b689ac3 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2541,6 +2541,66 @@ create_sort_path(PlannerInfo *root,
 	return pathnode;
 }
 
+TemporalAdjustmentPath *
+create_temporaladjustment_path(PlannerInfo *root,
+							   RelOptInfo *rel,
+							   Path *subpath,
+							   List *sortClause,
+							   TemporalClause *temporalClause)
+{
+	TemporalAdjustmentPath   *pathnode = makeNode(TemporalAdjustmentPath);
+
+	pathnode->path.pathtype = T_TemporalAdjustment;
+	pathnode->path.parent = rel;
+	/* TemporalAdjustment doesn't project, so use source path's pathtarget */
+	pathnode->path.pathtarget = subpath->pathtarget;
+	/* For now, assume we are above any joins, so no parameterization */
+	pathnode->path.param_info = NULL;
+
+	/* Currently we assume that temporal adjustment is not parallelizable */
+	pathnode->path.parallel_aware = false;
+	pathnode->path.parallel_safe = false;
+	pathnode->path.parallel_workers = 0;
+
+	/* Temporal Adjustment does not change the sort order */
+	pathnode->path.pathkeys = subpath->pathkeys;
+
+	pathnode->subpath = subpath;
+
+	/* Special information needed by temporal adjustment plan node */
+	pathnode->sortClause = copyObject(sortClause);
+	pathnode->temporalClause = copyObject(temporalClause);
+
+	/* Path's cost estimations */
+	pathnode->path.startup_cost = subpath->startup_cost;
+	pathnode->path.total_cost = subpath->total_cost;
+	pathnode->path.rows = subpath->rows;
+
+	if(temporalClause->temporalType == TEMPORAL_TYPE_ALIGNER)
+	{
+		/*
+		 * Every tuple from the sub-node can produce up to three tuples in the
+		 * algorithm. In addition we make up to three attribute comparisons for
+		 * each result tuple.
+		 */
+		pathnode->path.total_cost = subpath->total_cost +
+				(cpu_tuple_cost + 3 * cpu_operator_cost) * subpath->rows * 3;
+	}
+	else /* TEMPORAL_TYPE_NORMALIZER */
+	{
+		/*
+		 * For each split point in the sub-node we can have up to two result
+		 * tuples. The total cost is the cost of the sub-node plus for each
+		 * result tuple the cost to produce it and one attribute comparison
+		 * (different from alignment since we omit the intersection part).
+		 */
+		pathnode->path.total_cost = subpath->total_cost +
+				(cpu_tuple_cost + cpu_operator_cost) * subpath->rows * 2;
+	}
+
+	return pathnode;
+}
+
 /*
  * create_group_path
  *	  Creates a pathnode that represents performing grouping of presorted input
diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile
index 4b97f83..7416174 100644
--- a/src/backend/parser/Makefile
+++ b/src/backend/parser/Makefile
@@ -15,8 +15,8 @@ override CPPFLAGS := -I. -I$(srcdir) $(CPPFLAGS)
 OBJS= analyze.o gram.o scan.o parser.o \
       parse_agg.o parse_clause.o parse_coerce.o parse_collate.o parse_cte.o \
       parse_enr.o parse_expr.o parse_func.o parse_node.o parse_oper.o \
-      parse_param.o parse_relation.o parse_target.o parse_type.o \
-      parse_utilcmd.o scansup.o
+      parse_param.o parse_relation.o parse_target.o parse_temporal.o \
+      parse_type.o parse_utilcmd.o scansup.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 4fb793c..eee260b 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -40,6 +40,7 @@
 #include "parser/parse_param.h"
 #include "parser/parse_relation.h"
 #include "parser/parse_target.h"
+#include "parser/parse_temporal.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteManip.h"
 #include "utils/rel.h"
@@ -1217,6 +1218,9 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	/* mark column origins */
 	markTargetListOrigins(pstate, qry->targetList);
 
+	/* transform inner parts of a temporal primitive node */
+	qry->temporalClause = transformTemporalClause(pstate, qry, stmt);
+
 	/* transform WHERE */
 	qual = transformWhereClause(pstate, stmt->whereClause,
 								EXPR_KIND_WHERE, "WHERE");
@@ -1294,6 +1298,9 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
 	if (pstate->p_hasAggs || qry->groupClause || qry->groupingSets || qry->havingQual)
 		parseCheckAggregates(pstate, qry);
 
+	/* transform TEMPORAL PRIMITIVES */
+	qry->temporalClause = transformTemporalClauseResjunk(qry);
+
 	foreach(l, stmt->lockingClause)
 	{
 		transformLockingClause(pstate, qry,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4b1ce09..5743624 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -426,6 +426,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <boolean>	all_or_distinct
 
 %type <node>	join_outer join_qual
+%type <node>	normalizer_qual
 %type <jtype>	join_type
 
 %type <list>	extract_list overlay_list position_list
@@ -479,11 +480,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <value>	NumericOnly
 %type <list>	NumericOnly_list
 %type <alias>	alias_clause opt_alias_clause
+%type <list>	temporal_bounds
 %type <list>	func_alias_clause
 %type <sortby>	sortby
 %type <ielem>	index_elem
 %type <node>	table_ref
 %type <jexpr>	joined_table
+%type <jexpr>   aligned_table
+%type <jexpr>   normalized_table
 %type <range>	relation_expr
 %type <range>	relation_expr_opt_alias
 %type <node>	tablesample_clause opt_repeatable_clause
@@ -578,6 +582,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <partboundspec> ForValues
 %type <node>		partbound_datum PartitionRangeDatum
 %type <list>		partbound_datum_list range_datum_list
+%type <list>    temporal_bounds_list
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -602,7 +607,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 /* ordinary key words in alphabetical order */
 %token <keyword> ABORT_P ABSOLUTE_P ACCESS ACTION ADD_P ADMIN AFTER
-	AGGREGATE ALL ALSO ALTER ALWAYS ANALYSE ANALYZE AND ANY ARRAY AS ASC
+	AGGREGATE ALIGN ALL ALSO ALTER ALWAYS ANALYSE ANALYZE AND ANY ARRAY AS ASC
 	ASSERTION ASSIGNMENT ASYMMETRIC AT ATTACH ATTRIBUTE AUTHORIZATION
 
 	BACKWARD BEFORE BEGIN_P BETWEEN BIGINT BINARY BIT
@@ -648,7 +653,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	MAPPING MATCH MATERIALIZED MAXVALUE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE
 
-	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NO NONE
+	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NO NONE NORMALIZE
 	NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF
 	NULLS_P NUMERIC
 
@@ -11267,6 +11272,19 @@ first_or_next: FIRST_P								{ $$ = 0; }
 			| NEXT									{ $$ = 0; }
 		;
 
+temporal_bounds: WITH '(' temporal_bounds_list ')'				{ $$ = $3; }
+		;
+
+temporal_bounds_list:
+			columnref
+				{
+					$$ = list_make1($1);
+				}
+			| temporal_bounds_list ',' columnref
+				{
+					$$ = lappend($1, $3);
+				}
+		;
 
 /*
  * This syntax for group_clause tries to follow the spec quite closely.
@@ -11536,6 +11554,88 @@ table_ref:	relation_expr opt_alias_clause
 					$2->alias = $4;
 					$$ = (Node *) $2;
 				}
+			| '(' aligned_table ')' alias_clause
+				{
+					$2->alias = $4;
+					$$ = (Node *) $2;
+				}
+			| '(' normalized_table ')' alias_clause
+				{
+					$2->alias = $4;
+					$$ = (Node *) $2;
+				}
+		;
+
+aligned_table:
+			table_ref ALIGN table_ref ON a_expr temporal_bounds
+				{
+					JoinExpr *n = makeNode(JoinExpr);
+					n->jointype = TEMPORAL_ALIGN;
+					n->isNatural = FALSE;
+					n->larg = $1;
+					n->rarg = $3;
+
+					/* No USING clause, we use only ON as join qualifier. */
+					n->usingClause = NIL;
+
+					/*
+					 * A list for our valid-time boundaries with two range typed
+					 * values. Only PostgreSQL's default boundary type is
+					 * currently supported, i.e., '[)'.
+					 */
+					if(list_length($6) == 2)
+						n->temporalBounds = $6;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have two range typed values"),
+								 parser_errposition(@6)));
+
+					n->quals = $5; /* ON clause */
+					$$ = n;
+				}
+		;
+
+normalizer_qual:
+			USING '(' name_list ')'					{ $$ = (Node *) $3; }
+			| USING '(' ')'							{ $$ = (Node *) NIL; }
+			| ON a_expr								{ $$ = $2; }
+		;
+
+normalized_table:
+			table_ref NORMALIZE table_ref normalizer_qual temporal_bounds
+				{
+					JoinExpr *n = makeNode(JoinExpr);
+					n->jointype = TEMPORAL_NORMALIZE;
+					n->isNatural = FALSE;
+					n->larg = $1;
+					n->rarg = $3;
+
+					n->usingClause = NIL;
+					n->quals = NULL;
+
+					if ($4 != NULL && IsA($4, List))
+						n->usingClause = (List *) $4; /* USING clause */
+					else
+						n->quals = $4; /* ON clause */
+
+					/*
+					 * A list for our valid-time boundaries with two range typed
+					 * values. Only PostgreSQL's default boundary type is
+					 * currently supported, i.e., '[)'.
+					 */
+					if(list_length($5) == 2)
+						n->temporalBounds = $5;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have two range typed values"),
+								 parser_errposition(@5)));
+
+					$$ = n;
+				}
 		;
 
 
@@ -15005,7 +15105,8 @@ type_func_name_keyword:
  * forced to.
  */
 reserved_keyword:
-			  ALL
+			  ALIGN
+			| ALL
 			| ANALYSE
 			| ANALYZE
 			| AND
@@ -15053,6 +15154,7 @@ reserved_keyword:
 			| LIMIT
 			| LOCALTIME
 			| LOCALTIMESTAMP
+			| NORMALIZE
 			| NOT
 			| NULL_P
 			| OFFSET
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 9ff80b8..bbc5f77 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -40,6 +40,7 @@
 #include "parser/parse_func.h"
 #include "parser/parse_oper.h"
 #include "parser/parse_relation.h"
+#include "parser/parse_temporal.h"
 #include "parser/parse_target.h"
 #include "parser/parse_type.h"
 #include "rewrite/rewriteManip.h"
@@ -1247,6 +1248,43 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		int			k;
 
 		/*
+		 * If this is a temporal primitive, rewrite it into a sub-query using
+		 * the given join quals and the alias. We need this as temporal
+		 * primitives.
+		 */
+		if(j->jointype == TEMPORAL_ALIGN || j->jointype == TEMPORAL_NORMALIZE)
+		{
+			RangeSubselect			*rss;
+			RangeTblRef 			*rtr;
+			RangeTblEntry 			*rte;
+			int						 rtindex;
+
+			if(j->jointype == TEMPORAL_ALIGN)
+			{
+				/* Rewrite the temporal aligner into a sub-SELECT */
+				rss = (RangeSubselect *) transformTemporalAligner(pstate, j);
+			}
+			else
+			{
+				/* Rewrite the temporal normalizer into a sub-SELECT */
+				rss = (RangeSubselect *) transformTemporalNormalizer(pstate, j);
+			}
+
+			/* Transform the sub-SELECT */
+			rte = transformRangeSubselect(pstate, rss);
+
+			/* assume new rte is at end */
+			rtindex = list_length(pstate->p_rtable);
+			Assert(rte == rt_fetch(rtindex, pstate->p_rtable));
+			*top_rte = rte;
+			*top_rti = rtindex;
+			*namespace = list_make1(makeDefaultNSItem(rte));
+			rtr = makeNode(RangeTblRef);
+			rtr->rtindex = rtindex;
+			return (Node *) rtr;
+		}
+
+		/*
 		 * Recursively process the left subtree, then the right.  We must do
 		 * it in this order for correct visibility of LATERAL references.
 		 */
@@ -1309,6 +1347,16 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 				  &r_colnames, &r_colvars);
 
 		/*
+		 * Rename columns automatically to unique not-in-use column names, if
+		 * column names clash with internal-use-only columns of temporal
+		 * primitives.
+		 */
+		transformTemporalClauseAmbiguousColumns(pstate, j,
+												l_colnames, r_colnames,
+												l_colvars, r_colvars,
+												l_rte, r_rte);
+
+		/*
 		 * Natural join does not explicitly specify columns; must generate
 		 * columns to join. Need to run through the list of columns from each
 		 * table or join result and match up the column names. Use the first
diff --git a/src/backend/parser/parse_temporal.c b/src/backend/parser/parse_temporal.c
new file mode 100644
index 0000000..5b35938
--- /dev/null
+++ b/src/backend/parser/parse_temporal.c
@@ -0,0 +1,1347 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_temporal.c
+ *	  handle temporal operators in parser
+ *
+ * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/parser/parse_temporal.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "parser/parse_temporal.h"
+#include "parser/parsetree.h"
+#include "parser/parser.h"
+#include "parser/parse_type.h"
+#include "nodes/makefuncs.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_type.h"
+#include "utils/syscache.h"
+#include "utils/builtins.h"
+#include "access/htup_details.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/print.h"
+
+/*
+ * Enumeration of temporal boundary IDs. We have two elements in a boundary
+ * list (i.e., WITH-clause of a temporal primitive) as range-type boundaries,
+ * that is, VALID-TIME-attributes. In the future, we could even have
+ * a list with only one item. For instance, when we calculate temporal
+ * aggregations with a single attribute relation.
+ */
+typedef enum
+{
+	TPB_LARG_TIME = 0,
+	TPB_RARG_TIME,
+} TemporalBoundID;
+
+typedef enum
+{
+	TPB_ONERROR_NULL,
+	TPB_ONERROR_FAIL
+} TemporalBoundOnError;
+
+static void
+getColumnCounter(const char *colname,
+				 const char *prefix,
+				 bool *found,
+				 int *counter);
+
+static char *
+addTemporalAlias(ParseState *pstate,
+				 char *name,
+				 int counter);
+
+static SelectStmt *
+makeTemporalQuerySkeleton(JoinExpr *j,
+						  char **nameRN,
+						  char **nameP1,
+						  char **nameP2,
+						  Alias **largAlias,
+						  Alias **rargAlias);
+
+static ColumnRef *
+temporalBoundGet(List *bounds,
+				 TemporalBoundID id,
+				 TemporalBoundOnError oe);
+
+static char *
+temporalBoundGetName(List *bounds,
+					 TemporalBoundID id);
+
+static ColumnRef *
+temporalBoundGetCopyFQN(List *bounds,
+						TemporalBoundID id,
+						char *relname);
+
+static void
+temporalBoundCheckRelname(ColumnRef *bound,
+						  char *relname);
+
+static List *
+temporalBoundGetLeftBounds(List *bounds);
+
+static List *
+temporalBoundGetRightBounds(List *bounds);
+
+static void
+temporalBoundCheckIntegrity(ParseState *pstate,
+							List *bounds,
+							List *colnames,
+							List *colvars,
+							TemporalType tmpType);
+
+static Form_pg_type
+typeGet(Oid id);
+
+static List *
+internalUseOnlyColumnNames(ParseState *pstate,
+						   TemporalType tmpType);
+
+/*
+ * tpprint
+ * 		Temporal PostgreSQL print: pprint with surroundings to cut out pieces
+ * 		from long debug prints.
+ */
+void
+tpprint(const void *obj, const char *marker)
+{
+	printf("--------------------------------------SSS-%s\n", marker);
+	pprint(obj);
+	printf("--------------------------------------EEE-%s\n", marker);
+	fflush(stdout);
+}
+
+/*
+ * temporalBoundGetLeftBounds -
+ * 		Return the left boundaries of a temporal bounds list. This is a single
+ * 		range type value T holding both bounds.
+ */
+static List *
+temporalBoundGetLeftBounds(List *bounds)
+{
+	/* Invalid temporal bound list length? Specify two range-typed columns. */
+	Assert(list_length(bounds) == 2);
+	return list_make1(linitial(bounds));
+}
+
+/*
+ * temporalBoundGetRightBounds -
+ * 		Return the right boundaries of a temporal bounds list. This is a single
+ * 		range type value T holding both bounds.
+ */
+static List *
+temporalBoundGetRightBounds(List *bounds)
+{
+	/* Invalid temporal bound list length? Specify two range-typed columns. */
+	Assert(list_length(bounds) == 2);
+	return list_make1(lsecond(bounds));
+}
+
+/*
+ * temporalBoundCheckRelname -
+ * 		Check if full-qualified names within a boundary list (i.e., WITH-clause
+ * 		of a temporal primitive) match with the right or left argument
+ * 		respectively.
+ */
+static void
+temporalBoundCheckRelname(ColumnRef *bound, char *relname)
+{
+	char *givenRelname;
+	int l = list_length(bound->fields);
+
+	if(l == 1)
+		return;
+
+	givenRelname = strVal((Value *) list_nth(bound->fields, l - 2));
+
+	if(strcmp(relname, givenRelname) != 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+				 errmsg("The temporal bound \"%s\" does not match with " \
+						"the argument \"%s\" of the temporal primitive.",
+						 NameListToString(bound->fields), relname)));
+}
+
+/*
+ * temporalBoundGetCopyFQN -
+ * 		Creates a copy of a temporal bound from the boundary list identified
+ * 		with the given id. If it does not contain a full-qualified column
+ * 		reference, the last argument "relname" is used to build a new one.
+ */
+static ColumnRef *
+temporalBoundGetCopyFQN(List *bounds, TemporalBoundID id, char *relname)
+{
+	ColumnRef *bound = copyObject(temporalBoundGet(bounds, id,
+												   TPB_ONERROR_FAIL));
+	int l = list_length(bound->fields);
+
+	if(l == 1)
+		bound->fields = lcons(makeString(relname), bound->fields);
+	else
+		temporalBoundCheckRelname(bound, relname);
+
+	return bound;
+}
+
+/*
+ * temporalBoundGetName -
+ * 		Returns the name (that is, not the full-qualified column reference) of
+ * 		a bound.
+ */
+static char *
+temporalBoundGetName(List *bounds, TemporalBoundID id)
+{
+	ColumnRef *bound = temporalBoundGet(bounds, id, TPB_ONERROR_FAIL);
+	return strVal((Value *) llast(bound->fields));
+}
+
+/*
+ * temporalBoundGet -
+ * 		Returns a single bound with a given bound ID. See comments below for
+ * 		further details.
+ */
+static ColumnRef *
+temporalBoundGet(List *bounds, TemporalBoundID id, TemporalBoundOnError oe)
+{
+	int l = list_length(bounds);
+
+	switch(l)
+	{
+		/*
+		 * Two boundary entries are either two range-typed bounds, or a single
+		 * bound with two scalar values defining start and end (the later is
+		 * used for GROUP BY PERIOD for instance)
+		 */
+		case 2:
+			if(id == TPB_LARG_TIME)
+				return linitial(bounds);
+			if(id == TPB_RARG_TIME)
+				return lsecond(bounds);
+		break;
+
+		/*
+		 * One boundary entry is a range-typed bound for GROUP BY PERIOD or
+		 * DISTINCT PERIOD bounds.
+		 */
+		case 1:
+			if(id == TPB_LARG_TIME)
+				return linitial(bounds);
+	}
+
+	if (oe == TPB_ONERROR_FAIL)
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+			 errmsg("Invalid temporal bound list with length \"%d\" " \
+						"and index at \"%d\".", l, id),
+				 errhint("Specify two range-typed columns.")));
+
+	return NULL;
+}
+
+/*
+ * transformTemporalClause -
+ * 		If we have a temporal primitive query, we must find all attribute
+ * 		numbers for p1, p2, rn, and t columns. If the names of these
+ * 		internal-use-only columns are already occupied, we must rename them
+ * 		in order to not have an ambiguous column error.
+ *
+ * 		Please note: We cannot simply use resjunk columns here, because the
+ * 		subquery has already been build and parsed. We need these columns then
+ * 		for more than a single recursion step. This means, that we would loose
+ * 		resjunk columns too early. XXX PEMOSER Is there another possibility?
+ */
+Node *
+transformTemporalClause(ParseState *pstate, Query* qry, SelectStmt *stmt)
+{
+	ListCell   		*lc		   = NULL;
+	bool 			 foundTsTe = false;
+	TemporalClause  *tc		   = stmt->temporalClause;
+	int 			 pos;
+
+	/* No temporal clause given, do nothing */
+	if(!tc)
+		return NULL;
+
+	/* To start, the attribute number of temporal boundary is unknown */
+	tc->attNumTr = -1;
+
+	/*
+	 * Find attribute numbers for each attribute that is used during
+	 * temporal adjustment.
+	 */
+	pos = list_length(qry->targetList);
+	if (tc->temporalType == TEMPORAL_TYPE_ALIGNER)
+	{
+		tc->attNumP2 = pos--;
+		tc->attNumP1 = pos--;
+	}
+	else  /* Temporal normalizer */
+	{
+		/* This entry gets added during the sort-by transformation */
+		tc->attNumP1 = pos + 1;
+
+		/* Unknown and unused */
+		tc->attNumP2 = -1;
+	}
+
+	tc->attNumRN = pos;
+
+	/*
+	 * If we have temporal aliases stored in the current parser state, then we
+	 * got ambiguous columns. We resolve this problem by renaming parts of the
+	 * query tree with new unique column names.
+	 */
+	foreach(lc, pstate->p_temporal_aliases)
+	{
+		SortBy 		*sb 	= NULL;
+		char 		*key 	= strVal(linitial((List *) lfirst(lc)));
+		char 		*value 	= strVal(lsecond((List *) lfirst(lc)));
+		TargetEntry *tle 	= NULL;
+
+		if(strcmp(key, "rn") == 0)
+		{
+			sb = (SortBy *) linitial(stmt->sortClause);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumRN);
+		}
+		else if(strcmp(key, "p1") == 0)
+		{
+			sb = (SortBy *) lsecond(stmt->sortClause);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumP1);
+		}
+		else if(strcmp(key, "p2") == 0)
+		{
+			sb = (SortBy *) lthird(stmt->sortClause);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumP2);
+		}
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					 errmsg("Invalid column name \"%s\" for alias " \
+							"renames of temporal adjustment primitives.",
+							key)));
+
+		/*
+		 * Rename the order-by entry.
+		 * Just change the name if it is a column reference, nothing to do
+		 * for constants, i.e. if the group-by field has been specified by
+		 * a column attribute number (ex. 1 for the first column)
+		 */
+		if(sb && IsA(sb->node, ColumnRef))
+		{
+			ColumnRef *cr = (ColumnRef *) sb->node;
+			cr->fields = list_make1(makeString(value));
+		}
+
+		/*
+		 * Rename the targetlist entry for "p1", "p2", or "rn" iff aligner, and
+		 * rename it for both temporal primitives, if it is "ts" or "te".
+		 */
+		if(tle && (foundTsTe
+			|| tc->temporalType == TEMPORAL_TYPE_ALIGNER))
+		{
+			tle->resname = pstrdup(value);
+		}
+	}
+
+	/*
+	 * Find column attribute numbers of the two temporal attributes from
+	 * the left argument of the inner join, or the single temporal attribute if
+	 * it is a range type.
+	 */
+	foreach(lc, qry->targetList)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+		if(!tle->resname)
+			continue;
+
+		if (strcmp(tle->resname, tc->colnameTr) == 0)
+			tc->attNumTr = tle->resno;
+	}
+
+	/* We need column attribute numbers for all temporal boundaries */
+	if(tc->colnameTr && tc->attNumTr == -1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT),
+				 errmsg("Needed columns for temporal adjustment not found.")));
+
+	return (Node *) tc;
+}
+
+/*
+ * transformTemporalClauseResjunk -
+ * 		If we have a temporal primitive query, the last three columns are P1,
+ * 		P2, and row_id or RN, which we do not need anymore after temporal
+ * 		adjustment operations have been accomplished.
+ *      However, if the temporal boundaries are range typed columns we split
+ *      the range [ts, te) into two separate columns ts and te, which must be
+ *      marked as resjunk too.
+ *      XXX PEMOSER Use a single loop inside!
+ */
+Node *
+transformTemporalClauseResjunk(Query *qry)
+{
+	TemporalClause 	*tc = (TemporalClause *) qry->temporalClause;
+
+	/* No temporal clause given, do nothing */
+	if(!tc)
+		return NULL;
+
+	/* Mark P1 and RN columns as junk, we do not need them afterwards. */
+	get_tle_by_resno(qry->targetList, tc->attNumP1)->resjunk = true;
+	get_tle_by_resno(qry->targetList, tc->attNumRN)->resjunk = true;
+
+	/* An aligner has also a P2 column, that must be marked as junk. */
+	if (tc->temporalType == TEMPORAL_TYPE_ALIGNER)
+		get_tle_by_resno(qry->targetList, tc->attNumP2)->resjunk = true;
+
+	/*
+	 * Pass the temporal primitive node to the optimizer, to be used later,
+	 * to mark unsafe columns, and add attribute indexes.
+	 */
+	return (Node *) tc;
+}
+
+/*
+ * addTemporalAlias -
+ * 		We use internal-use-only columns to store some information used for
+ * 		temporal primitives. Since we need them over several sub-queries, we
+ * 		cannot use simply resjunk columns here. We must rename parts of the
+ * 		parse tree to handle ambiguous columns. In order to reference the right
+ * 		columns after renaming, we store them inside the current parser state,
+ * 		and use them afterwards to rename fields. Such attributes could be for
+ * 		example: P1, P2, or RN.
+ */
+static char *
+addTemporalAlias(ParseState *pstate, char *name, int counter)
+{
+	char 	*newName = palloc(64);
+
+	/*
+	 * Column name for <name> alternative is <name>_N, where N is 0 if no
+	 * other column with that pattern has been found, or N + 1 if
+	 * the highest number for a <name>_N column is N. N stand for the <counter>.
+	 */
+	counter++;
+	sprintf(newName, "%s_%d", name, counter);
+
+	/*
+	 * Changed aliases must be remembered by the parser state in
+	 * order to use them on nodes above, i.e. if they are used in targetlists,
+	 * group-by or order-by clauses outside.
+	 */
+	pstate->p_temporal_aliases =
+			lappend(pstate->p_temporal_aliases,
+					list_make2(makeString(name),
+							   makeString(newName)));
+
+	return newName;
+}
+
+/*
+ * getColumnCounter -
+ * 		Check if a column name starts with a certain prefix. If it ends after
+ * 		the prefix, return found (we ignore the counter in this case). However,
+ * 		if it continuous with an underscore check if it has a tail after it that
+ * 		is a string representation of an integer. If so, return this number as
+ * 		integer (keep the parameter "found" as is).
+ * 		We use this function to rename "internal-use-only" columns on an
+ * 		ambiguity error with user-specified columns.
+ */
+static void
+getColumnCounter(const char *colname, const char *prefix,
+				 bool *found, int *counter)
+{
+	if(memcmp(colname, prefix, strlen(prefix)) == 0)
+	{
+		colname += strlen(prefix);
+		if(*colname == '\0')
+			*found = true;
+		else if (*colname++ == '_')
+		{
+			char 	*pos;
+			int 	 n = -1;
+
+			errno = 0;
+			n = strtol(colname, &pos, 10);
+
+			/*
+			 * No error and fully parsed (i.e., string contained
+			 * only an integer) => save it if it is bigger than
+			 * the last.
+			 */
+			if(errno == 0 && *pos == 0 && n > *counter)
+				*counter = n;
+		}
+	}
+}
+
+/*
+ * Creates a skeleton query that can be filled with needed fields from both
+ * temporal primitives. This is the common part of both generated to re-use
+ * the same code. It also returns palloc'd names for p1, p2, and rn, where p2
+ * is optional (omit it by passing NULL).
+ *
+ * OUTPUT:
+ * 		(
+ * 		SELECT r.*
+ *      FROM
+ *      (
+ *      	SELECT *, row_id() OVER () rn FROM r
+ *      ) r
+ *      LEFT OUTER JOIN
+ *      <not set yet>
+ *      ON <not set yet>
+ *      ORDER BY rn, p1
+ *      ) x
+ */
+static SelectStmt *
+makeTemporalQuerySkeleton(JoinExpr *j, char **nameRN, char **nameP1,
+						  char **nameP2, Alias **largAlias, Alias **rargAlias)
+{
+	const int 	 		 UNKNOWN_LOCATION = -1;
+
+	SelectStmt			*ssJoinLarg;
+	SelectStmt 			*ssRowNumber;
+	SelectStmt			*ssResult;
+	RangeSubselect 		*rssJoinLarg;
+	RangeSubselect 		*rssRowNumber;
+	ResTarget			*rtRowNumber;
+	ResTarget			*rtAStar;
+	ResTarget			*rtAStarWithR;
+	ColumnRef 			*crAStarWithR;
+	ColumnRef 			*crAStar;
+	WindowDef			*wdRowNumber;
+	FuncCall			*fcRowNumber;
+	JoinExpr			*joinExpr;
+	SortBy				*sb1;
+	SortBy				*sb2;
+
+	/*
+	 * These attribute names could cause conflicts, if the left or right
+	 * relation has column names like these. We solve this later by renaming
+	 * column names when we know which columns are in use, in order to create
+	 * unique column names.
+	 */
+	*nameRN = pstrdup("rn");
+	*nameP1 = pstrdup("p1");
+	if(nameP2) *nameP2 = pstrdup("p2");
+
+	/* Find aliases of arguments */
+	*largAlias = makeAliasFromArgument(j->larg);
+	*rargAlias = makeAliasFromArgument(j->rarg);
+
+	/*
+	 * Build "(SELECT row_id() OVER (), * FROM r) r".
+	 * We start with building the resource target for "*".
+	 */
+	crAStar = makeColumnRef1((Node *) makeNode(A_Star));
+	rtAStar = makeResTarget((Node *) crAStar, NULL);
+
+	/* Build an empty window definition clause, i.e. "OVER ()" */
+	wdRowNumber = makeNode(WindowDef);
+	wdRowNumber->frameOptions = FRAMEOPTION_DEFAULTS;
+	wdRowNumber->startOffset = NULL;
+	wdRowNumber->endOffset = NULL;
+
+	/*
+	 * Build a target for "row_id() OVER ()", row_id() enumerates each tuple
+	 * similar to row_number().
+	 * The rowid-function is push-down-safe, because we need only unique ids for
+	 * each tuple, and do not care about gaps between numbers.
+	 */
+	fcRowNumber = makeFuncCall(SystemFuncName("row_id"),
+							   NIL,
+							   UNKNOWN_LOCATION);
+	fcRowNumber->over = wdRowNumber;
+	rtRowNumber = makeResTarget((Node *) fcRowNumber, NULL);
+	rtRowNumber->name = *nameRN;
+
+	/*
+	 * Build sub-select clause with from- and where-clause from the
+	 * outer query. Add "row_id() OVER ()" to the target list.
+	 */
+	ssRowNumber = makeNode(SelectStmt);
+	ssRowNumber->fromClause = list_make1(j->larg);
+	ssRowNumber->groupClause = NIL;
+	ssRowNumber->whereClause = NULL;
+	ssRowNumber->targetList = list_make2(rtAStar, rtRowNumber);
+
+	/* Build range sub-select */
+	rssRowNumber = makeNode(RangeSubselect);
+	rssRowNumber->subquery = (Node *) ssRowNumber;
+	rssRowNumber->alias = *largAlias;
+	rssRowNumber->lateral = false;
+
+	/* Build resource target for "r.*" */
+	crAStarWithR = makeColumnRef2((Node *) makeString((*largAlias)->aliasname),
+								  (Node *) makeNode(A_Star));
+	rtAStarWithR = makeResTarget((Node *) crAStarWithR, NULL);
+
+	/* Build the outer range sub-select */
+	ssJoinLarg = makeNode(SelectStmt);
+	ssJoinLarg->fromClause = list_make1(rssRowNumber);
+	ssJoinLarg->groupClause = NIL;
+	ssJoinLarg->whereClause = NULL;
+
+	/* Build range sub-select */
+	rssJoinLarg = makeNode(RangeSubselect);
+	rssJoinLarg->subquery = (Node *) ssJoinLarg;
+	rssJoinLarg->lateral = false;
+
+	/* Build a join expression */
+	joinExpr = makeNode(JoinExpr);
+	joinExpr->isNatural = false;
+	joinExpr->larg = (Node *) rssRowNumber;
+	joinExpr->jointype = JOIN_LEFT; /* left outer join */
+
+	/*
+	 * Copy temporal bounds into temporal primitive subquery join in order to
+	 * compare temporal bound var types with actual target list var types. We
+	 * do this to trigger an error on type mismatch, before a subquery function
+	 * fails and triggers an non-meaningful error (as for example, "operator
+	 * does not exists, or similar").
+	 */
+	joinExpr->temporalBounds = copyObject(j->temporalBounds);
+
+	sb1 = makeNode(SortBy);
+	sb1->location = UNKNOWN_LOCATION;
+	sb1->node = (Node *) makeColumnRef1((Node *) makeString(*nameRN));
+
+	sb2 = makeNode(SortBy);
+	sb2->location = UNKNOWN_LOCATION;
+	sb2->node = (Node *) makeColumnRef1((Node *) makeString(*nameP1));
+
+	ssResult = makeNode(SelectStmt);
+	ssResult->withClause = NULL;
+	ssResult->fromClause = list_make1(joinExpr);
+	ssResult->targetList = list_make1(rtAStarWithR);
+	ssResult->sortClause = list_make2(sb1, sb2);
+	ssResult->temporalClause = makeNode(TemporalClause);
+
+	/*
+	 * Hardcoded column names for ts and te. We handle ambiguous column
+	 * names during the transformation of temporal primitive clauses.
+	 */
+	ssResult->temporalClause->colnameTr =
+			temporalBoundGetName(j->temporalBounds, TPB_LARG_TIME);
+
+	/*
+	 * We mark the outer sub-query with the current temporal adjustment type,
+	 * s.t. the optimizer understands that we need the corresponding temporal
+	 * adjustment node above.
+	 */
+	ssResult->temporalClause->temporalType =
+			j->jointype == TEMPORAL_ALIGN ? TEMPORAL_TYPE_ALIGNER
+										  : TEMPORAL_TYPE_NORMALIZER;
+
+	/* Let the join inside a temporal primitive know which type its parent has */
+	joinExpr->inTmpPrimTempType = ssResult->temporalClause->temporalType;
+
+	return ssResult;
+}
+
+/*
+ * transformTemporalAligner -
+ * 		transform a TEMPORAL ALIGN clause into standard SQL
+ *
+ * INPUT:
+ * 		(r ALIGN s ON q WITH (r.t, s.t)) c
+ *
+ *      where r and s are input relations, q can be any
+ *      join qualifier, and r.t, s.t can be any column name. The latter
+ *      represent the valid time intervals, that is time point start,
+ *      and time point end of each tuple for each input relation. These
+ *      are two half-open, i.e., [), range typed values.
+ *
+ * OUTPUT:
+ *      (
+ * 		SELECT r.*, GREATEST(LOWER(r.t), LOWER(s.t)) P1,
+ * 		            LEAST(UPPER(r.t), UPPER(s.t)) P2
+ *      FROM
+ *      (
+ *      	SELECT *, row_id() OVER () rn FROM r
+ *      ) r
+ *      LEFT OUTER JOIN
+ *      s
+ *      ON q AND r.t && s.t
+ *      ORDER BY rn, P1, P2
+ *      ) c
+ */
+Node *
+transformTemporalAligner(ParseState *pstate, JoinExpr *j)
+{
+	const int 	 		 UNKNOWN_LOCATION = -1;
+
+	SelectStmt			*ssResult;
+	RangeSubselect 		*rssResult;
+	ResTarget			*rtGreatest;
+	ResTarget			*rtLeast;
+	ColumnRef 			*crLargTs;
+	ColumnRef 			*crRargTs;
+	MinMaxExpr			*mmeGreatest;
+	MinMaxExpr			*mmeLeast;
+	FuncCall			*fcLowerLarg;
+	FuncCall			*fcLowerRarg;
+	FuncCall			*fcUpperLarg;
+	FuncCall			*fcUpperRarg;
+	List				*mmeGreatestArgs;
+	List				*mmeLeastArgs;
+	List				*boundariesExpr;
+	JoinExpr			*joinExpr;
+	A_Expr				*overlapExpr;
+	Node				*boolExpr;
+	SortBy				*sb3;
+	Alias				*largAlias = NULL;
+	Alias				*rargAlias = NULL;
+	char 				*colnameRN;
+	char 				*colnameP1;
+	char 				*colnameP2;
+
+	/* Create a select statement skeleton to be filled here */
+	ssResult = makeTemporalQuerySkeleton(j, &colnameRN, &colnameP1,
+										 &colnameP2,
+										 &largAlias, &rargAlias);
+
+	/* Temporal aligners do not support the USING-clause */
+	Assert(j->usingClause == NIL);
+
+	/*
+	 * Build column references, for use later. If we need only two range types
+	 * only Ts columnrefs are used.
+	 */
+	crLargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARG_TIME,
+									   largAlias->aliasname);
+	crRargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARG_TIME,
+									   rargAlias->aliasname);
+
+	/* Create argument list for function call to "greatest" and "least" */
+	fcLowerLarg = makeFuncCall(SystemFuncName("lower"),
+							   list_make1(crLargTs),
+							   UNKNOWN_LOCATION);
+	fcLowerRarg = makeFuncCall(SystemFuncName("lower"),
+							   list_make1(crRargTs),
+							   UNKNOWN_LOCATION);
+	fcUpperLarg = makeFuncCall(SystemFuncName("upper"),
+							   list_make1(crLargTs),
+							   UNKNOWN_LOCATION);
+	fcUpperRarg = makeFuncCall(SystemFuncName("upper"),
+							   list_make1(crRargTs),
+							   UNKNOWN_LOCATION);
+	mmeGreatestArgs = list_make2(fcLowerLarg, fcLowerRarg);
+	mmeLeastArgs = list_make2(fcUpperLarg, fcUpperRarg);
+
+	overlapExpr = makeSimpleA_Expr(AEXPR_OP,
+								   "&&",
+								   copyObject(crLargTs),
+								   copyObject(crRargTs),
+								   UNKNOWN_LOCATION);
+
+	boundariesExpr = list_make1(overlapExpr);
+
+	/* Concatenate all Boolean expressions by AND */
+	boolExpr = (Node *) makeBoolExpr(AND_EXPR,
+									 lappend(boundariesExpr, j->quals),
+									 UNKNOWN_LOCATION);
+
+	/* Build the function call "greatest(r.ts, s.ts) P1" */
+	mmeGreatest = makeNode(MinMaxExpr);
+	mmeGreatest->args = mmeGreatestArgs;
+	mmeGreatest->location = UNKNOWN_LOCATION;
+	mmeGreatest->op = IS_GREATEST;
+	rtGreatest = makeResTarget((Node *) mmeGreatest, NULL);
+	rtGreatest->name = colnameP1;
+
+	/* Build the function call "least(r.te, s.te) P2" */
+	mmeLeast = makeNode(MinMaxExpr);
+	mmeLeast->args = mmeLeastArgs;
+	mmeLeast->location = UNKNOWN_LOCATION;
+	mmeLeast->op = IS_LEAST;
+	rtLeast = makeResTarget((Node *) mmeLeast, NULL);
+	rtLeast->name = colnameP2;
+
+	sb3 = makeNode(SortBy);
+	sb3->location = UNKNOWN_LOCATION;
+	sb3->node = (Node *) makeColumnRef1((Node *) makeString(colnameP2));
+
+	ssResult->targetList = list_concat(ssResult->targetList,
+									   list_make2(rtGreatest, rtLeast));
+	ssResult->sortClause = lappend(ssResult->sortClause, sb3);
+
+	joinExpr = (JoinExpr *) linitial(ssResult->fromClause);
+	joinExpr->rarg = copyObject(j->rarg);
+	joinExpr->quals = boolExpr;
+
+	/* Build range sub-select */
+	rssResult = makeNode(RangeSubselect);
+	rssResult->subquery = (Node *) ssResult;
+	rssResult->alias = copyObject(j->alias);
+	rssResult->lateral = false;
+
+	return copyObject(rssResult);
+}
+
+/*
+ * transformTemporalNormalizer -
+ * 		transform a TEMPORAL NORMALIZE clause into standard SQL
+ *
+ * INPUT:
+ * 		(r NORMALIZE s ON q WITH (r.t, s.t)) c
+ *
+ * 		-- or --
+ *
+ * 		(r NORMALIZE s USING(atts) WITH (r.t, s.t)) c
+ *
+ *      where r and s are input relations, q can be any
+ *      join qualifier, atts are a list of column names (like in a
+ *      join-using-clause), and r.t, and s.t can be any column name.
+ *      The latter represent the valid time intervals, that is time
+ *      point start, and time point end of each tuple for each input
+ *      relation. These are two half-open, i.e., [), range typed values.
+ *
+ * OUTPUT:
+ * 		(
+ * 			SELECT r.*
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT s.*, LOWER(s.t) P1 FROM s
+ *      		UNION ALL
+ *      		SELECT s.*, UPPER(s.t) P1 FROM s
+ *      	) s
+ *      	ON q AND P1 <@ t
+ *      	ORDER BY rn, P1
+ *      ) c
+ *
+ *      -- or --
+ *
+ * 		(
+ * 			SELECT r.*
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT atts, LOWER(s.t) P1 FROM s
+ *      		UNION
+ *      		SELECT atts, UPPER(s.t) P1 FROM s
+ *      	) s
+ *      	ON r.atts = s.atts AND P1 <@ t
+ *      	ORDER BY rn, P1
+ *      ) c
+ *
+ */
+Node *
+transformTemporalNormalizer(ParseState *pstate, JoinExpr *j)
+{
+	const int 	 		 UNKNOWN_LOCATION = -1;
+
+	SelectStmt			*ssTsP1;
+	SelectStmt			*ssTeP1;
+	SelectStmt			*ssUnionAll;
+	SelectStmt			*ssResult;
+	RangeSubselect 		*rssUnionAll;
+	RangeSubselect 		*rssResult;
+	ResTarget			*rtRargStar;
+	ResTarget			*rtTsP1;
+	ResTarget			*rtTeP1;
+	ColumnRef 			*crRargStar;
+	ColumnRef 			*crLargTsT = NULL;
+	ColumnRef 			*crRargTsT = NULL;
+	ColumnRef 			*crP1;
+	JoinExpr			*joinExpr;
+	A_Expr				*containsExpr;
+	Node				*boolExpr;
+	Alias				*largAlias;
+	Alias				*rargAlias;
+	char 				*colnameRN;
+	char 				*colnameP1;
+	FuncCall			*fcLowerRarg = NULL;
+	FuncCall			*fcUpperRarg = NULL;
+	List				*boundariesExpr;
+
+	/* Create a select statement skeleton to be filled here */
+	ssResult = makeTemporalQuerySkeleton(j, &colnameRN, &colnameP1,
+										 NULL, &largAlias, &rargAlias);
+
+	/* Build resource target for "s.*" to use it later. */
+	crRargStar = makeColumnRef2((Node *) makeString(rargAlias->aliasname),
+								(Node *) makeNode(A_Star));
+
+	crP1 = makeColumnRef1((Node *) makeString(colnameP1));
+
+	/* Build column references, for use later. */
+	crLargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARG_TIME,
+									   largAlias->aliasname);
+	crRargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARG_TIME,
+									   rargAlias->aliasname);
+
+	/* Create argument list for function call to "lower" and "upper" */
+	fcLowerRarg = makeFuncCall(SystemFuncName("lower"),
+							   list_make1(crRargTsT),
+							   UNKNOWN_LOCATION);
+	fcUpperRarg = makeFuncCall(SystemFuncName("upper"),
+							   list_make1(crRargTsT),
+							   UNKNOWN_LOCATION);
+
+	/* Build resource target "lower(s.t) P1" and "upper(s.t) P1" */
+	rtTsP1 = makeResTarget((Node *) fcLowerRarg, colnameP1);
+	rtTeP1 = makeResTarget((Node *) fcUpperRarg, colnameP1);
+
+	/*
+	 * Build "contains" expression for range types, i.e. "P1 <@ t"
+	 * and concatenate it with q (=theta)
+	 */
+	containsExpr = makeSimpleA_Expr(AEXPR_OP,
+									"<@",
+									copyObject(crP1),
+									copyObject(crLargTsT),
+									UNKNOWN_LOCATION);
+
+	boundariesExpr = list_make1(containsExpr);
+
+	/*
+	 * For ON-clause notation build
+	 * 		"SELECT s.*, lower(t) P1 FROM s", and
+	 * 		"SELECT s.*, upper(t) P1 FROM s".
+	 * For USING-clause with a name-list 'atts' build
+	 * 		"SELECT atts, lower(t) P1 FROM s", and
+	 * 		"SELECT atts, upper(t) P1 FROM s".
+	 */
+	ssTsP1 = makeNode(SelectStmt);
+	ssTsP1->fromClause = list_make1(j->rarg);
+	ssTsP1->groupClause = NIL;
+	ssTsP1->whereClause = NULL;
+
+	ssTeP1 = copyObject(ssTsP1);
+
+	if (j->usingClause)
+	{
+		ListCell   *usingItem;
+		A_Expr     *expr;
+		List	   *qualList = NIL;
+		char	   *colnameTr = ssResult->temporalClause->colnameTr;
+
+		Assert(j->quals == NULL); 	/* shouldn't have ON() too */
+
+		foreach(usingItem, j->usingClause)
+		{
+			char		*usingItemName = strVal(lfirst(usingItem));
+			ColumnRef   *crUsingItemL =
+					makeColumnRef2((Node *) makeString(largAlias->aliasname),
+								   (Node *) makeString(usingItemName));
+			ColumnRef   *crUsingItemR =
+					makeColumnRef2((Node *) makeString(rargAlias->aliasname),
+								   (Node *) makeString(usingItemName));
+			ResTarget	*rtUsingItemR = makeResTarget((Node *) crUsingItemR,
+													  NULL);
+
+			/*
+			 * Skip temporal attributes, because temporal normalizer's USING
+			 * list must contain only non-temporal attributes. We allow
+			 * temporal attributes as input, such that we can copy colname lists
+			 * to create temporal normalizers easier.
+			 */
+			if(colnameTr && strcmp(usingItemName, colnameTr) == 0)
+				continue;
+
+			expr = makeSimpleA_Expr(AEXPR_OP,
+									 "=",
+									 copyObject(crUsingItemL),
+									 copyObject(crUsingItemR),
+									 UNKNOWN_LOCATION);
+
+			qualList = lappend(qualList, expr);
+
+			ssTsP1->targetList = lappend(ssTsP1->targetList, rtUsingItemR);
+			ssTeP1->targetList = lappend(ssTeP1->targetList, rtUsingItemR);
+		}
+
+		j->quals = (Node *) makeBoolExpr(AND_EXPR, qualList, UNKNOWN_LOCATION);
+	}
+	else if (j->quals)
+	{
+		rtRargStar = makeResTarget((Node *) crRargStar, NULL);
+		ssTsP1->targetList = list_make1(rtRargStar);
+		ssTeP1->targetList = list_make1(rtRargStar);
+	}
+
+	ssTsP1->targetList = lappend(ssTsP1->targetList, rtTsP1);
+	ssTeP1->targetList = lappend(ssTeP1->targetList, rtTeP1);
+
+	/*
+	 * Build sub-select for "( SELECT ... UNION [ALL] SELECT ... ) s", i.e.,
+	 * build an union between two select-clauses, i.e. a select-clause with
+	 * set-operation set to "union".
+	 */
+	ssUnionAll = makeNode(SelectStmt);
+	ssUnionAll->op = SETOP_UNION;
+	ssUnionAll->all = j->usingClause == NIL;	/* true, if ON-clause */
+	ssUnionAll->larg = ssTsP1;
+	ssUnionAll->rarg = ssTeP1;
+
+	/* Build range sub-select for "( ...UNION [ALL]... ) s" */
+	rssUnionAll = makeNode(RangeSubselect);
+	rssUnionAll->subquery = (Node *) ssUnionAll;
+	rssUnionAll->alias = rargAlias;
+	rssUnionAll->lateral = false;
+
+	/*
+	 * Create a conjunction of all Boolean expressions
+	 */
+	if (j->quals)
+	{
+		boolExpr = (Node *) makeBoolExpr(AND_EXPR,
+										 lappend(boundariesExpr, j->quals),
+										 UNKNOWN_LOCATION);
+	}
+	else	/* empty USING() clause found, i.e. theta = true */
+	{
+		boolExpr = (Node *) makeBoolExpr(AND_EXPR,
+										 boundariesExpr,
+										 UNKNOWN_LOCATION);
+		ssUnionAll->all = false;
+
+	}
+
+	joinExpr = (JoinExpr *) linitial(ssResult->fromClause);
+	joinExpr->rarg = (Node *) rssUnionAll;
+	joinExpr->quals = boolExpr;
+
+	/* Build range sub-select */
+	rssResult = makeNode(RangeSubselect);
+	rssResult->subquery = (Node *) ssResult;
+	rssResult->alias = copyObject(j->alias);
+	rssResult->lateral = false;
+
+	return copyObject(rssResult);
+}
+
+/*
+ * typeGet -
+ * 		Return the type of a tuple from the system cache for a given OID.
+ */
+static Form_pg_type
+typeGet(Oid id)
+{
+	HeapTuple	tp;
+	Form_pg_type typtup;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(id));
+	if (!HeapTupleIsValid(tp))
+		ereport(ERROR,
+				(errcode(ERROR),
+				 errmsg("cache lookup failed for type %u", id)));
+
+	typtup = (Form_pg_type) GETSTRUCT(tp);
+	ReleaseSysCache(tp);
+	return typtup;
+}
+
+/*
+ * internalUseOnlyColumnNames -
+ * 		Creates a list of all internal-use-only column names, depending on the
+ * 		temporal primitive type (i.e., normalizer or aligner). The list is then
+ * 		compared with the aliases from the current parser state, and renamed
+ * 		if necessary.
+ */
+static List *
+internalUseOnlyColumnNames(ParseState *pstate,
+						   TemporalType tmpType)
+{
+	List		*filter = NIL;
+	ListCell	*lcFilter;
+	ListCell	*lcAlias;
+
+	filter = list_make2(makeString("rn"), makeString("p1"));
+
+	if(tmpType == TEMPORAL_TYPE_ALIGNER)
+		filter = lappend(filter, makeString("p2"));
+
+	foreach(lcFilter, filter)
+	{
+		Value	*filterValue = (Value *) lfirst(lcFilter);
+		char	*filterName = strVal(filterValue);
+
+		foreach(lcAlias, pstate->p_temporal_aliases)
+		{
+			char 	*aliasKey 	= strVal(linitial((List *) lfirst(lcAlias)));
+			char 	*aliasValue = strVal(lsecond((List *) lfirst(lcAlias)));
+
+			if(strcmp(filterName, aliasKey) == 0)
+				filterValue->val.str = pstrdup(aliasValue);
+		}
+	}
+
+	return filter;
+}
+
+/*
+ * temporalBoundCheckIntegrity -
+ * 		For each column name check if it is a temporal bound. If so, check
+ * 		also if it does not clash with an internal-use-only column name, and if
+ * 		the attribute types match with the range type predicate.
+ */
+static void
+temporalBoundCheckIntegrity(ParseState *pstate,
+							 List *bounds,
+							 List *colnames,
+							 List *colvars,
+							 TemporalType tmpType)
+{
+	ListCell 	*lcNames;
+	ListCell 	*lcVars;
+	ListCell 	*lcBound;
+	ListCell 	*lcFilter;
+	List		*filter = internalUseOnlyColumnNames(pstate, tmpType);
+
+	forboth(lcNames, colnames, lcVars, colvars)
+	{
+		char *name = strVal((Value *) lfirst(lcNames));
+		Var	 *var  = (Var *) lfirst(lcVars);
+
+		foreach(lcBound, bounds)
+		{
+			ColumnRef 	*crb = (ColumnRef *) lfirst(lcBound);
+			char 		*nameb = strVal((Value *) llast(crb->fields));
+
+			if(strcmp(nameb, name) == 0)
+			{
+				char 				*msg = "";
+				Form_pg_type		 type;
+
+				foreach(lcFilter, filter)
+				{
+					char	*n = strVal((Value *) lfirst(lcFilter));
+					if(strcmp(n, name) == 0)
+						ereport(ERROR,
+								(errcode(ERRCODE_UNDEFINED_COLUMN),
+								 errmsg("column \"%s\" does not exist", n),
+								 parser_errposition(pstate, crb->location)));
+				}
+
+				type = typeGet(var->vartype);
+
+				if(type->typtype != TYPTYPE_RANGE)
+					msg = "Invalid column type \"%s\" for the temporal bound " \
+						  "\"%s\". It must be a range type column.";
+
+				if (strlen(msg) > 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+							 errmsg(msg,
+									NameStr(type->typname),
+									NameListToString(crb->fields)),
+							 errhint("Specify two range-typed columns."),
+							 parser_errposition(pstate, crb->location)));
+
+			}
+		}
+	}
+
+}
+
+
+/*
+ * transformTemporalClauseAmbiguousColumns -
+ * 		Rename columns automatically to unique not-in-use column names, if
+ * 		column names clash with internal-use-only columns of temporal
+ * 		primitives.
+ */
+void
+transformTemporalClauseAmbiguousColumns(ParseState* pstate, JoinExpr* j,
+										List* l_colnames, List* r_colnames,
+										List *l_colvars, List *r_colvars,
+										RangeTblEntry* l_rte,
+										RangeTblEntry* r_rte)
+{
+	ListCell   *l = NULL;
+	bool 		foundP1 = false;
+	bool 		foundP2 = false;
+	bool 		foundRN = false;
+	int 		counterP1 = -1;
+	int 		counterP2 = -1;
+	int 		counterRN = -1;
+
+	/* Nothing to do, if we have no temporal primitive */
+	if (j->inTmpPrimTempType == TEMPORAL_TYPE_NONE)
+		return;
+
+	/*
+	 * Check ambiguity of column names, search for p1, p2, and rn
+	 * columns and rename them accordingly to X_N, where X = {p1,p2,rn},
+	 * and N is the highest number after X_ starting from 0. This is, if we do
+	 * not find any X_N column pattern the new column is renamed to X_0.
+	 */
+	foreach(l, l_colnames)
+	{
+		const char *colname = strVal((Value *) lfirst(l));
+
+		/*
+		 * Skip the last entry of the left column names, i.e. row_id
+		 * is only an internally added column by both temporal
+		 * primitives.
+		 */
+		if (l == list_tail(l_colnames))
+			continue;
+
+		getColumnCounter(colname, "p1", &foundP1, &counterP1);
+		getColumnCounter(colname, "rn", &foundRN, &counterRN);
+
+		/* Only temporal aligners have a p2 column */
+		if (j->inTmpPrimTempType == TEMPORAL_TYPE_ALIGNER)
+			getColumnCounter(colname, "p2", &foundP2, &counterP2);
+	}
+
+	foreach(l, r_colnames)
+	{
+		const char *colname = strVal((Value *) lfirst(l));
+
+		/*
+		 * The temporal normalizer adds also a column called p1 which is
+		 * the union of te and ts interval boundaries. We ignore it here
+		 * since it does not belong to the user defined columns of the
+		 * given input, iff it is the last entry of the column list.
+		 */
+		if (j->inTmpPrimTempType == TEMPORAL_TYPE_NORMALIZER
+				&& l == list_tail(r_colnames))
+			continue;
+
+		getColumnCounter(colname, "p1", &foundP1, &counterP1);
+		getColumnCounter(colname, "rn", &foundRN, &counterRN);
+
+		/* Only temporal aligners have a p2 column */
+		if (j->inTmpPrimTempType == TEMPORAL_TYPE_ALIGNER)
+			getColumnCounter(colname, "p2", &foundP2, &counterP2);
+	}
+
+	if (foundP1)
+	{
+		char *name = addTemporalAlias(pstate, "p1", counterP1);
+
+		/*
+		 * The right subtree gets now a new name for the column p1.
+		 * In addition, we rename both expressions used for temporal
+		 * boundary checks. It is fixed that they are at the end of this
+		 * join's qualifier list.
+		 * Only temporal normalization needs these steps.
+		 */
+		if (j->inTmpPrimTempType == TEMPORAL_TYPE_NORMALIZER)
+		{
+			A_Expr *e1;
+			List *qualArgs;
+
+			llast(r_rte->eref->colnames) = makeString(name);
+			llast(r_colnames) = makeString(name);
+
+			qualArgs = ((BoolExpr *) j->quals)->args;
+			e1 = (A_Expr *) linitial(qualArgs);
+			linitial(((ColumnRef *)e1->lexpr)->fields) = makeString(name);
+		}
+	}
+
+	if (foundRN)
+	{
+		char *name = addTemporalAlias(pstate, "rn", counterRN);
+
+		/* The left subtree has now a new name for the column rn */
+		llast(l_rte->eref->colnames) = makeString(name);
+		llast(l_colnames) = makeString(name);
+	}
+
+	if (foundP2)
+		addTemporalAlias(pstate, "p2", counterP2);
+
+	temporalBoundCheckIntegrity(pstate,
+								temporalBoundGetLeftBounds(j->temporalBounds),
+								l_colnames, l_colvars, j->inTmpPrimTempType);
+
+
+	temporalBoundCheckIntegrity(pstate,
+								temporalBoundGetRightBounds(j->temporalBounds),
+								r_colnames, r_colvars, j->inTmpPrimTempType);
+
+}
+
+/*
+ * makeTemporalNormalizer -
+ *		Creates a temporal normalizer join expression.
+ *		XXX PEMOSER Should we create a separate temporal primitive expression?
+ */
+JoinExpr *
+makeTemporalNormalizer(Node *larg, Node *rarg, List *bounds, Node *quals,
+					   Alias *alias)
+{
+	JoinExpr *j = makeNode(JoinExpr);
+
+	if(! ((IsA(larg, RangeSubselect) || IsA(larg, RangeVar)) &&
+		  (IsA(rarg, RangeSubselect) || IsA(rarg, RangeVar))))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("Normalizer arguments must be of type RangeVar or " \
+					   "RangeSubselect.")));
+
+	j->jointype = TEMPORAL_NORMALIZE;
+
+	/*
+	 * Qualifiers can be an boolean expression or an USING clause, i.e. a list
+	 * of column names.
+	 */
+	if(quals == (Node *) NIL || IsA(quals, List))
+		j->usingClause = (List *) quals;
+	else
+		j->quals = quals;
+
+	j->larg = larg;
+	j->rarg = rarg;
+	j->alias = alias;
+	j->temporalBounds = bounds;
+
+	return j;
+}
+
+/*
+ * makeTemporalAligner -
+ *		Creates a temporal aligner join expression.
+ *		XXX PEMOSER Should we create a separate temporal primitive expression?
+ */
+JoinExpr *
+makeTemporalAligner(Node *larg, Node *rarg, List *bounds, Node *quals,
+					Alias *alias)
+{
+	JoinExpr *j = makeNode(JoinExpr);
+
+	if(! ((IsA(larg, RangeSubselect) || IsA(larg, RangeVar)) &&
+		  (IsA(rarg, RangeSubselect) || IsA(rarg, RangeVar))))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("Aligner arguments must be of type RangeVar or " \
+					   "RangeSubselect.")));
+
+	j->jointype = TEMPORAL_ALIGN;
+
+	/* Empty quals allowed (i.e., NULL), but no LISTS */
+	if(quals && IsA(quals, List))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("Aligner do not support an USING clause.")));
+	else
+		j->quals = quals;
+
+	j->larg = larg;
+	j->rarg = rarg;
+	j->alias = alias;
+	j->temporalBounds = bounds;
+
+	return j;
+}
+
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index d86ad70..c2f5f79 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -88,6 +88,19 @@ window_row_number(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(curpos + 1);
 }
 
+/*
+ * row_id
+ * just increment up from 1 until current partition finishes.
+ */
+Datum
+window_row_id(PG_FUNCTION_ARGS)
+{
+	WindowObject winobj = PG_WINDOW_OBJECT();
+	int64		curpos = WinGetCurrentPosition(winobj);
+
+	WinSetMarkPosition(winobj, curpos);
+	PG_RETURN_INT64(curpos + 1);
+}
 
 /*
  * rank
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 4f35471..c93550d 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -205,6 +205,7 @@ Section: Class 22 - Data Exception
 2200N    E    ERRCODE_INVALID_XML_CONTENT                                    invalid_xml_content
 2200S    E    ERRCODE_INVALID_XML_COMMENT                                    invalid_xml_comment
 2200T    E    ERRCODE_INVALID_XML_PROCESSING_INSTRUCTION                     invalid_xml_processing_instruction
+220T0    E    ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT               invalid_argument_for_temporal_adjustment
 
 Section: Class 23 - Integrity Constraint Violation
 
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 8b33b4e..174005a 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -5102,6 +5102,8 @@ DATA(insert OID = 3113 (  last_value	PGNSP PGUID 12 1 0 0 0 f t f f t f i s 1 0
 DESCR("fetch the last row value");
 DATA(insert OID = 3114 (  nth_value		PGNSP PGUID 12 1 0 0 0 f t f f t f i s 2 0 2283 "2283 23" _null_ _null_ _null_ _null_ _null_ window_nth_value _null_ _null_ _null_ ));
 DESCR("fetch the Nth row value");
+DATA(insert OID = 4999 (  row_id		PGNSP PGUID 12 1 0 0 0 f t f f f f i s 0 0 20 "" _null_ _null_ _null_ _null_ _null_ window_row_id _null_ _null_ _null_ ));
+DESCR("row id within partition");
 
 /* functions for range types */
 DATA(insert OID = 3832 (  anyrange_in	PGNSP PGUID 12 1 0 0 0 f f f f t f s s 3 0 3831 "2275 26 23" _null_ _null_ _null_ _null_ _null_ anyrange_in _null_ _null_ _null_ ));
diff --git a/src/include/executor/nodeTemporalAdjustment.h b/src/include/executor/nodeTemporalAdjustment.h
new file mode 100644
index 0000000..7a4be3d
--- /dev/null
+++ b/src/include/executor/nodeTemporalAdjustment.h
@@ -0,0 +1,24 @@
+/*-------------------------------------------------------------------------
+ *
+ * nodeTemporalAdjustment.h
+ *
+ *
+ *
+ * Portions Copyright (c) 1996-2015, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/executor/nodeLimit.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef NODETEMPORALADJUSTMENT_H
+#define NODETEMPORALADJUSTMENT_H
+
+#include "nodes/execnodes.h"
+
+extern TemporalAdjustmentState *ExecInitTemporalAdjustment(TemporalAdjustment *node, EState *estate, int eflags);
+extern TupleTableSlot *ExecTemporalAdjustment(TemporalAdjustmentState *node);
+extern void ExecEndTemporalAdjustment(TemporalAdjustmentState *node);
+extern void ExecReScanTemporalAdjustment(TemporalAdjustmentState *node);
+
+#endif   /* NODETEMPORALADJUSTMENT_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 35c28a6..027f66c 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1099,6 +1099,32 @@ typedef struct ScanState
 } ScanState;
 
 /* ----------------
+ *	 TemporalAdjustmentState information
+ * ----------------
+ */
+typedef struct TemporalAdjustmentState
+{
+	ScanState 		 	  ss;
+	bool 			 	  firstCall;	  /* Setup on first call already done? */
+	bool 			 	  alignment;	  /* true = align; false = normalize */
+	bool 			 	  sameleft;		  /* Is the previous and current tuple
+											 from the same group? */
+	Datum 			 	  sweepline;	  /* Sweep line status */
+	int64			 	  outrn;		  /* temporal aligner group-id */
+	TemporalClause		 *temporalCl;
+	bool 				 *nullMask;		  /* See heap_modify_tuple */
+	bool 				 *tsteMask;		  /* See heap_modify_tuple */
+	Datum 				 *newValues;	  /* tuple values that get updated */
+	MemoryContext		  tempContext;
+	FunctionCallInfoData  eqFuncCallInfo; /* calling equal */
+	FunctionCallInfoData  ltFuncCallInfo; /* calling less-than */
+	FunctionCallInfoData  rcFuncCallInfo; /* calling range_constructor2 */
+	FunctionCallInfoData  loFuncCallInfo; /* calling lower(range) */
+	FunctionCallInfoData  upFuncCallInfo; /* calling upper(range) */
+	Form_pg_attribute     datumFormat;	  /* Datum format of sweepline, P1, P2 */
+} TemporalAdjustmentState;
+
+/* ----------------
  *	 SeqScanState information
  * ----------------
  */
diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h
index 46a79b1..2704d44 100644
--- a/src/include/nodes/makefuncs.h
+++ b/src/include/nodes/makefuncs.h
@@ -85,5 +85,9 @@ extern DefElem *makeDefElemExtended(char *nameSpace, char *name, Node *arg,
 					DefElemAction defaction, int location);
 
 extern GroupingSet *makeGroupingSet(GroupingSetKind kind, List *content, int location);
+extern ColumnRef *makeColumnRef1(Node *field1);
+extern ColumnRef *makeColumnRef2(Node *field1, Node *field2);
+extern ResTarget *makeResTarget(Node *val, char *name);
+extern Alias *makeAliasFromArgument(Node *arg);
 
 #endif							/* MAKEFUNC_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 27bd4f3..6a042f6 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -83,6 +83,7 @@ typedef enum NodeTag
 	T_SetOp,
 	T_LockRows,
 	T_Limit,
+	T_TemporalAdjustment,
 	/* these aren't subclasses of Plan: */
 	T_NestLoopParam,
 	T_PlanRowMark,
@@ -135,6 +136,7 @@ typedef enum NodeTag
 	T_SetOpState,
 	T_LockRowsState,
 	T_LimitState,
+	T_TemporalAdjustmentState,
 
 	/*
 	 * TAGS FOR PRIMITIVE NODES (primnodes.h)
@@ -251,6 +253,7 @@ typedef enum NodeTag
 	T_LockRowsPath,
 	T_ModifyTablePath,
 	T_LimitPath,
+	T_TemporalAdjustmentPath,
 	/* these aren't subclasses of Path: */
 	T_EquivalenceClass,
 	T_EquivalenceMember,
@@ -468,6 +471,7 @@ typedef enum NodeTag
 	T_PartitionBoundSpec,
 	T_PartitionRangeDatum,
 	T_PartitionCmd,
+	T_TemporalClause,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
@@ -698,7 +702,14 @@ typedef enum JoinType
 	 * by the executor (nor, indeed, by most of the planner).
 	 */
 	JOIN_UNIQUE_OUTER,			/* LHS path must be made unique */
-	JOIN_UNIQUE_INNER			/* RHS path must be made unique */
+	JOIN_UNIQUE_INNER,			/* RHS path must be made unique */
+
+
+	/*
+	 * Temporal adjustment primitives
+	 */
+	TEMPORAL_ALIGN,
+	TEMPORAL_NORMALIZE
 
 	/*
 	 * We might need additional join types someday.
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 5f2a4a7..19ad163 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -170,6 +170,8 @@ typedef struct Query
 									 * only added during rewrite and therefore
 									 * are not written out as part of Query. */
 
+	Node	   *temporalClause; /* temporal primitive node */
+
 	/*
 	 * The following two fields identify the portion of the source text string
 	 * containing this query.  They are typically only populated in top-level
@@ -1532,6 +1534,8 @@ typedef struct SelectStmt
 	List	   *lockingClause;	/* FOR UPDATE (list of LockingClause's) */
 	WithClause *withClause;		/* WITH clause */
 
+	TemporalClause *temporalClause; /* Temporal primitive node */
+
 	/*
 	 * These fields are used only in upper-level SelectStmts.
 	 */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index f1a1b24..be482fd 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -238,6 +238,24 @@ typedef struct ModifyTable
 } ModifyTable;
 
 /* ----------------
+ *	 TemporalAdjustment node -
+ *		Generate a temporal adjustment node as temporal aligner or normalizer.
+ * ----------------
+ */
+typedef struct TemporalAdjustment
+{
+	Plan			 plan;
+	int     		 numCols;    	  /* number of columns in total */
+	Oid        		 eqOperatorID;    /* equality operator to compare with */
+	Oid        		 ltOperatorID;    /* less-than operator to compare with */
+	Oid              sortCollationID; /* sort operator collation id */
+	TemporalClause  *temporalCl;	  /* Temporal type, attribute numbers,
+										 and colnames */
+	Var             *rangeVar;		  /* targetlist entry of the given range
+										 type used to call range_constructor */
+} TemporalAdjustment;
+
+/* ----------------
  *	 Append node -
  *		Generate the concatenation of the results of sub-plans.
  * ----------------
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 8c536a8..e880814 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -52,6 +52,31 @@ typedef enum OnCommitAction
 	ONCOMMIT_DROP				/* ON COMMIT DROP */
 } OnCommitAction;
 
+/* Options for temporal primitives used by queries with temporal alignment */
+typedef enum TemporalType
+{
+	TEMPORAL_TYPE_NONE,
+	TEMPORAL_TYPE_ALIGNER,
+	TEMPORAL_TYPE_NORMALIZER
+} TemporalType;
+
+typedef struct TemporalClause
+{
+	NodeTag      type;
+	TemporalType temporalType;   /* Type of temporal primitives */
+
+	/*
+	 * Attribute number or column position for internal-use-only columns, and
+	 * temporal boundaries
+	 */
+	AttrNumber   attNumTr;
+	AttrNumber   attNumP1;
+	AttrNumber   attNumP2;
+	AttrNumber   attNumRN;
+
+	char		*colnameTr;	    /* If range type used for bounds, or NULL */
+} TemporalClause;
+
 /*
  * RangeVar - range variable, used in FROM clauses
  *
@@ -1454,6 +1479,9 @@ typedef struct JoinExpr
 	Node	   *quals;			/* qualifiers on join, if any */
 	Alias	   *alias;			/* user-written alias clause, if any */
 	int			rtindex;		/* RT index assigned for join, or 0 */
+	List	   *temporalBounds; /* columns that form bounds for both subtrees,
+								 * used by temporal adjustment primitives */
+	TemporalType inTmpPrimTempType;	/* inside a temporal primitive clause */
 } JoinExpr;
 
 /*----------
diff --git a/src/include/nodes/print.h b/src/include/nodes/print.h
index fa01c2a..d4212c2 100644
--- a/src/include/nodes/print.h
+++ b/src/include/nodes/print.h
@@ -30,5 +30,6 @@ extern void print_expr(const Node *expr, const List *rtable);
 extern void print_pathkeys(const List *pathkeys, const List *rtable);
 extern void print_tl(const List *tlist, const List *rtable);
 extern void print_slot(TupleTableSlot *slot);
+extern void print_namespace(const List *namespace);
 
 #endif							/* PRINT_H */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 9bae3c6..3ff2cf6 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1116,6 +1116,25 @@ typedef struct SubqueryScanPath
 } SubqueryScanPath;
 
 /*
+ * TemporalAdjustmentPath represents a scan of a rewritten temporal subquery.
+ *
+ * Depending, whether it is a temporal normalizer or a temporal aligner, we have
+ * different subqueries below the temporal adjustment node, but for sure there
+ * is a sort clause on top of the rewritten subquery for both temporal
+ * primitives. We remember this sort clause, because we need to fetch equality,
+ * sort operator, and collation Oids from it. Which will then re-used for the
+ * temporal primitive clause.
+ */
+typedef struct TemporalAdjustmentPath
+{
+	Path			 path;
+	Path	   		*subpath;		/* path representing subquery execution */
+	List	   		*sortClause;
+	TemporalClause 	*temporalClause;
+} TemporalAdjustmentPath;
+
+
+/*
  * ForeignPath represents a potential scan of a foreign table, foreign join
  * or foreign upper-relation.
  *
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 0c0549d..5b58170 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -168,6 +168,11 @@ extern SortPath *create_sort_path(PlannerInfo *root,
 				 Path *subpath,
 				 List *pathkeys,
 				 double limit_tuples);
+extern TemporalAdjustmentPath *create_temporaladjustment_path(PlannerInfo *root,
+						RelOptInfo *rel,
+						Path *subpath,
+						List *sortClause,
+						TemporalClause *temporalClause);
 extern GroupPath *create_group_path(PlannerInfo *root,
 				  RelOptInfo *rel,
 				  Path *subpath,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f50e45e..54f633d 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -34,6 +34,7 @@ PG_KEYWORD("add", ADD_P, UNRESERVED_KEYWORD)
 PG_KEYWORD("admin", ADMIN, UNRESERVED_KEYWORD)
 PG_KEYWORD("after", AFTER, UNRESERVED_KEYWORD)
 PG_KEYWORD("aggregate", AGGREGATE, UNRESERVED_KEYWORD)
+PG_KEYWORD("align", ALIGN, RESERVED_KEYWORD)
 PG_KEYWORD("all", ALL, RESERVED_KEYWORD)
 PG_KEYWORD("also", ALSO, UNRESERVED_KEYWORD)
 PG_KEYWORD("alter", ALTER, UNRESERVED_KEYWORD)
@@ -259,6 +260,7 @@ PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD)
 PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD)
 PG_KEYWORD("no", NO, UNRESERVED_KEYWORD)
 PG_KEYWORD("none", NONE, COL_NAME_KEYWORD)
+PG_KEYWORD("normalize", NORMALIZE, RESERVED_KEYWORD)
 PG_KEYWORD("not", NOT, RESERVED_KEYWORD)
 PG_KEYWORD("nothing", NOTHING, UNRESERVED_KEYWORD)
 PG_KEYWORD("notify", NOTIFY, UNRESERVED_KEYWORD)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 68930c1..0bcd036 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -204,6 +204,12 @@ struct ParseState
 	Node	   *p_last_srf;		/* most recent set-returning func/op found */
 
 	/*
+	 * Temporal aliases for internal-use-only columns (used by temporal
+	 * primitives only.
+	 */
+	List	   *p_temporal_aliases;
+
+	/*
 	 * Optional hook functions for parser callbacks.  These are null unless
 	 * set up by the caller of make_parsestate.
 	 */
diff --git a/src/include/parser/parse_temporal.h b/src/include/parser/parse_temporal.h
new file mode 100644
index 0000000..235831e
--- /dev/null
+++ b/src/include/parser/parse_temporal.h
@@ -0,0 +1,62 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_temporal.h
+ *	  handle temporal operators in parser
+ *
+ *
+ * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/parser/parse_temporal.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PARSE_TEMPORAL_H
+#define PARSE_TEMPORAL_H
+
+#include "parser/parse_node.h"
+
+extern Node *
+transformTemporalClauseResjunk(Query* qry);
+
+extern Node *
+transformTemporalClause(ParseState *pstate,
+						Query *qry,
+						SelectStmt *stmt);
+
+extern Node *
+transformTemporalAligner(ParseState *pstate,
+						 JoinExpr *j);
+
+extern Node *
+transformTemporalNormalizer(ParseState *pstate,
+							JoinExpr *j);
+
+extern void
+transformTemporalClauseAmbiguousColumns(ParseState *pstate,
+										JoinExpr *j,
+										List *l_colnames,
+										List *r_colnames,
+										List *l_colvars,
+										List *r_colvars,
+										RangeTblEntry *l_rte,
+										RangeTblEntry *r_rte);
+
+extern JoinExpr *
+makeTemporalNormalizer(Node *larg,
+					   Node *rarg,
+					   List *bounds,
+					   Node *quals,
+					   Alias *alias);
+
+extern JoinExpr *
+makeTemporalAligner(Node *larg,
+					Node *rarg,
+					List *bounds,
+					Node *quals,
+					Alias *alias);
+
+extern void
+tpprint(const void *obj, const char *marker);
+
+#endif   /* PARSE_TEMPORAL_H */
diff --git a/src/test/regress/expected/temporal_primitives.out b/src/test/regress/expected/temporal_primitives.out
new file mode 100644
index 0000000..3600b76
--- /dev/null
+++ b/src/test/regress/expected/temporal_primitives.out
@@ -0,0 +1,739 @@
+--
+-- TEMPORAL PRIMITIVES: ALIGN AND NORMALIZE
+--
+SET datestyle TO ymd;
+CREATE TYPE varcharrange AS RANGE (SUBTYPE=varchar);
+CREATE TYPE floatrange AS RANGE (SUBTYPE = float8, SUBTYPE_DIFF = float8mi);
+CREATE TEMP TABLE table1_int4r (a char, b char, t int4range);
+CREATE TEMP TABLE table2_int4r (c int, d char, t int4range);
+INSERT INTO table1_int4r VALUES
+('a','B','[1,7)'),
+('b','B','[3,9)'),
+('c','G','[8,10)');
+INSERT INTO table2_int4r VALUES
+(1,'B','[2,5)'),
+(2,'B','[3,4)'),
+(3,'B','[7,9)');
+-- VALID TIME columns (i.e., ts and te) are no longer at the end of the
+-- targetlist.
+CREATE TEMP TABLE table1_int4r_mix AS SELECT a, t, b FROM table1_int4r;
+CREATE TEMP TABLE table2_int4r_mix AS SELECT t, c, d FROM table2_int4r;
+-- VALID TIME columns as VARCHARs
+CREATE TEMP TABLE table1_varcharr (a int, t varcharrange);
+CREATE TEMP TABLE table2_varcharr (a int, t varcharrange);
+INSERT INTO table1_varcharr VALUES
+(0, varcharrange('A', 'D')),
+(1, varcharrange('C', 'X')),
+(0, varcharrange('ABC', 'BCD')),
+(0, varcharrange('xABC', 'xBCD')),
+(0, varcharrange('BAA', 'BBB'));
+INSERT INTO table2_varcharr VALUES
+(0, varcharrange('A', 'D')),
+(1, varcharrange('C', 'X'));
+-- Tables to check different data types, and corner cases
+CREATE TEMP TABLE table_tsrange (a int, t tsrange);
+CREATE TEMP TABLE table1_int4r0 (a int, t floatrange);
+CREATE TEMP TABLE table1_int4r1 AS TABLE table1_int4r0;
+INSERT INTO table_tsrange VALUES
+(0, '[2000-01-01, 2000-01-10)'),
+(1, '[2000-01-05, 2000-01-20)');
+INSERT INTO table1_int4r0 VALUES
+(0, floatrange(1.0, 1.1111)),
+(1, floatrange(1.11109999, 2.0));
+INSERT INTO table1_int4r1 VALUES
+(0, floatrange(1.0, 'Infinity')),
+(1, floatrange('-Infinity', 2.0));
+--
+-- TEMPORAL ALIGNER: BASICS
+--
+-- Equality qualifiers
+SELECT * FROM (
+	table1_int4r ALIGN table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x;
+ a | b |   t    
+---+---+--------
+ a | B | [1,2)
+ a | B | [2,5)
+ a | B | [3,4)
+ a | B | [5,7)
+ b | B | [3,4)
+ b | B | [3,5)
+ b | B | [5,7)
+ b | B | [7,9)
+ c | G | [8,10)
+(9 rows)
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+SELECT * FROM (
+	table1_int4r ALIGN table2_int4r
+		ON table1_int4r.b = table2_int4r.d
+		WITH (table1_int4r.t, table2_int4r.t)
+	) x;
+ a | b |   t    
+---+---+--------
+ a | B | [1,2)
+ a | B | [2,5)
+ a | B | [3,4)
+ a | B | [5,7)
+ b | B | [3,4)
+ b | B | [3,5)
+ b | B | [5,7)
+ b | B | [7,9)
+ c | G | [8,10)
+(9 rows)
+
+-- Alignment with aggregation
+-- NB: Targetlist of outer query is *not* A_STAR...
+SELECT a, COUNT(a) FROM (
+	table1_int4r ALIGN table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x
+	GROUP BY a ORDER BY a;
+ a | count 
+---+-------
+ a |     4
+ b |     4
+ c |     1
+(3 rows)
+
+-- Equality qualifiers
+-- Test column positions where ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT * FROM (
+	table1_int4r_mix ALIGN table2_int4r_mix
+		ON b = d
+		WITH (t, t)
+	) x;
+ a |   t    | b 
+---+--------+---
+ a | [1,2)  | B
+ a | [2,5)  | B
+ a | [3,4)  | B
+ a | [5,7)  | B
+ b | [3,4)  | B
+ b | [3,5)  | B
+ b | [5,7)  | B
+ b | [7,9)  | B
+ c | [8,10) | G
+(9 rows)
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+-- Test column positions where ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT * FROM (
+	table1_int4r_mix ALIGN table2_int4r_mix
+		ON table1_int4r_mix.b = table2_int4r_mix.d
+		WITH (table1_int4r_mix.t, table2_int4r_mix.t)
+	) x;
+ a |   t    | b 
+---+--------+---
+ a | [1,2)  | B
+ a | [2,5)  | B
+ a | [3,4)  | B
+ a | [5,7)  | B
+ b | [3,4)  | B
+ b | [3,5)  | B
+ b | [5,7)  | B
+ b | [7,9)  | B
+ c | [8,10) | G
+(9 rows)
+
+-- Alignment with aggregation where targetlist of outer query is *not* A_STAR...
+-- Test column positions where ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT a, COUNT(a) FROM (
+	table1_int4r_mix ALIGN table2_int4r_mix
+		ON b = d
+		WITH (t, t)
+	) x
+	GROUP BY a ORDER BY a;
+ a | count 
+---+-------
+ a |     4
+ b |     4
+ c |     1
+(3 rows)
+
+-- Test relations with differently named temporal bound attributes and relation
+-- and column aliases.
+SELECT * FROM (
+	table1_int4r ALIGN table2_int4r x(c,d,s)
+		ON b = d
+		WITH (t, s)
+	) x;
+ a | b |   t    
+---+---+--------
+ a | B | [1,2)
+ a | B | [2,5)
+ a | B | [3,4)
+ a | B | [5,7)
+ b | B | [3,4)
+ b | B | [3,5)
+ b | B | [5,7)
+ b | B | [7,9)
+ c | G | [8,10)
+(9 rows)
+
+--
+-- TEMPORAL ALIGNER: TEMPORAL JOIN EXAMPLE
+--
+-- Full temporal join example with absorbing where clause, timestamp
+-- propagation (see CTEs targetlists with V and U) and range types
+WITH t1 AS (SELECT *, t u FROM table1_int4r),
+	 t2 AS (SELECT c a, d b, t, t v FROM table2_int4r)
+SELECT t, b, x.a, y.a FROM (
+	t1 ALIGN t2
+		ON t1.b = t2.b
+		WITH (t, t)
+	) x
+	LEFT OUTER JOIN (
+		SELECT * FROM (
+		t2 ALIGN t1
+			ON t1.b = t2.b
+			WITH (t, t)
+		) y
+	) y
+	USING (b, t)
+	WHERE (
+			(lower(t) = lower(u) OR lower(t) = lower(v))
+			AND
+			(upper(t) = upper(u) OR upper(t) = upper(v))
+		)
+		OR u IS NULL
+		OR v IS NULL
+	ORDER BY 1,2,3,4;
+   t    | b | a | a 
+--------+---+---+---
+ [1,2)  | B | a |  
+ [2,5)  | B | a | 1
+ [3,4)  | B | a | 2
+ [3,4)  | B | b | 2
+ [3,5)  | B | b | 1
+ [5,7)  | B | a |  
+ [5,7)  | B | b |  
+ [7,9)  | B | b | 3
+ [8,10) | G | c |  
+(9 rows)
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	table1_varcharr x ALIGN table1_varcharr y
+		ON TRUE
+		WITH (t, t)
+	) x;
+ a |      t      
+---+-------------
+ 0 | [A,D)
+ 0 | [ABC,BCD)
+ 0 | [BAA,BBB)
+ 0 | [C,D)
+ 1 | [C,D)
+ 1 | [C,X)
+ 0 | [ABC,BCD)
+ 0 | [BAA,BBB)
+ 0 | [xABC,xBCD)
+ 0 | [BAA,BBB)
+(10 rows)
+
+--
+-- TEMPORAL ALIGNER: SELECTION PUSH-DOWN
+--
+-- VALID TIME columns are not safe to be pushed down, for the rest everything
+-- should work as usual.
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r ALIGN table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3;
+                                                                          QUERY PLAN                                                                          
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(lower(table2_int4r.t), lower(table1_int4r.t))), (LEAST(upper(table2_int4r.t), upper(table1_int4r.t)))
+               ->  Nested Loop Left Join
+                     Join Filter: (table2_int4r.t && table1_int4r.t)
+                     ->  WindowAgg
+                           ->  Seq Scan on table2_int4r
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Seq Scan on table1_int4r
+(11 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r ALIGN table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3 AND lower(t) > 3;
+                                                                          QUERY PLAN                                                                          
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: (lower(x.t) > 3)
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(lower(table2_int4r.t), lower(table1_int4r.t))), (LEAST(upper(table2_int4r.t), upper(table1_int4r.t)))
+               ->  Nested Loop Left Join
+                     Join Filter: (table2_int4r.t && table1_int4r.t)
+                     ->  WindowAgg
+                           ->  Seq Scan on table2_int4r
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Seq Scan on table1_int4r
+(12 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r ALIGN table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3 OR lower(t) > 3;
+                                                                          QUERY PLAN                                                                          
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: ((x.c < 3) OR (lower(x.t) > 3))
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(lower(table2_int4r.t), lower(table1_int4r.t))), (LEAST(upper(table2_int4r.t), upper(table1_int4r.t)))
+               ->  Nested Loop Left Join
+                     Join Filter: (table2_int4r.t && table1_int4r.t)
+                     ->  WindowAgg
+                           ->  Seq Scan on table2_int4r
+                     ->  Materialize
+                           ->  Seq Scan on table1_int4r
+(11 rows)
+
+--
+-- TEMPORAL ALIGNER: DATA TYPES
+--
+-- Data types: Timestamps
+-- We use to_char here to be sure that we have the same output format on all
+-- platforms and locale configuration
+SELECT a, to_char(lower(t), 'YYYY-MM-DD') ts, to_char(upper(t), 'YYYY-MM-DD') te
+FROM (
+	table_tsrange t1 ALIGN table_tsrange t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+ a |     ts     |     te     
+---+------------+------------
+ 0 | 2000-01-01 | 2000-01-10
+ 0 | 2000-01-05 | 2000-01-10
+ 1 | 2000-01-05 | 2000-01-20
+(3 rows)
+
+-- Data types: Double precision
+SELECT a, t FROM (
+	table1_int4r0 t1 ALIGN table1_int4r0 t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+ a |          t          
+---+---------------------
+ 0 | [1,1.1111)
+ 0 | [1.11109999,1.1111)
+ 1 | [1.11109999,2)
+(3 rows)
+
+-- Data types: Double precision with +/- infinity
+SELECT a, t FROM (
+	table1_int4r1 t1 ALIGN table1_int4r1 t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+ a |       t       
+---+---------------
+ 0 | [1,2)
+ 0 | [1,Infinity)
+ 1 | [-Infinity,2)
+(3 rows)
+
+--
+-- TEMPORAL NORMALIZER: BASICS
+--
+-- Equality qualifiers
+SELECT * FROM (
+	table1_int4r NORMALIZE table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x;
+ a | b |   t    
+---+---+--------
+ a | B | [1,2)
+ a | B | [2,3)
+ a | B | [3,4)
+ a | B | [4,5)
+ a | B | [5,7)
+ b | B | [3,4)
+ b | B | [4,5)
+ b | B | [5,7)
+ b | B | [7,9)
+ c | G | [8,10)
+(10 rows)
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+SELECT * FROM (
+	table1_int4r NORMALIZE table2_int4r
+		ON table1_int4r.b = table2_int4r.d
+		WITH (table1_int4r.t, table2_int4r.t)
+	) x;
+ a | b |   t    
+---+---+--------
+ a | B | [1,2)
+ a | B | [2,3)
+ a | B | [3,4)
+ a | B | [4,5)
+ a | B | [5,7)
+ b | B | [3,4)
+ b | B | [4,5)
+ b | B | [5,7)
+ b | B | [7,9)
+ c | G | [8,10)
+(10 rows)
+
+-- Alignment with aggregation
+-- NB: Targetlist of outer query is *not* A_STAR...
+SELECT a, COUNT(a) FROM (
+	table1_int4r NORMALIZE table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x
+	GROUP BY a ORDER BY a;
+ a | count 
+---+-------
+ a |     5
+ b |     4
+ c |     1
+(3 rows)
+
+-- Equality qualifiers
+-- Test column positions where ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT * FROM (
+	table1_int4r_mix NORMALIZE table2_int4r_mix
+		ON b = d
+		WITH (t, t)
+	) x;
+ a |   t    | b 
+---+--------+---
+ a | [1,2)  | B
+ a | [2,3)  | B
+ a | [3,4)  | B
+ a | [4,5)  | B
+ a | [5,7)  | B
+ b | [3,4)  | B
+ b | [4,5)  | B
+ b | [5,7)  | B
+ b | [7,9)  | B
+ c | [8,10) | G
+(10 rows)
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+-- Test column positions where t is not at the last column.
+-- Please note: This was a restriction in an early implementation.
+SELECT * FROM (
+	table1_int4r_mix NORMALIZE table2_int4r_mix
+		ON table1_int4r_mix.b = table2_int4r_mix.d
+		WITH (table1_int4r_mix.t, table2_int4r_mix.t)
+	) x;
+ a |   t    | b 
+---+--------+---
+ a | [1,2)  | B
+ a | [2,3)  | B
+ a | [3,4)  | B
+ a | [4,5)  | B
+ a | [5,7)  | B
+ b | [3,4)  | B
+ b | [4,5)  | B
+ b | [5,7)  | B
+ b | [7,9)  | B
+ c | [8,10) | G
+(10 rows)
+
+-- Alignment with aggregation where targetlist of outer query is *not* A_STAR...
+-- Test column positions where t is not at the last column.
+-- Please note: This was a restriction in an early implementation.
+SELECT a, COUNT(a) FROM (
+	table1_int4r_mix NORMALIZE table2_int4r_mix
+		ON b = d
+		WITH (t, t)
+	) x
+	GROUP BY a ORDER BY a;
+ a | count 
+---+-------
+ a |     5
+ b |     4
+ c |     1
+(3 rows)
+
+-- Test relations with differently named temporal bound attributes and relation
+-- and column aliases.
+SELECT * FROM (
+	table1_int4r NORMALIZE table2_int4r x(c,d,s)
+		ON b = d
+		WITH (t, s)
+	) x;
+ a | b |   t    
+---+---+--------
+ a | B | [1,2)
+ a | B | [2,3)
+ a | B | [3,4)
+ a | B | [4,5)
+ a | B | [5,7)
+ b | B | [3,4)
+ b | B | [4,5)
+ b | B | [5,7)
+ b | B | [7,9)
+ c | G | [8,10)
+(10 rows)
+
+-- Normalizer's USING clause (self-normalization)
+SELECT * FROM (
+	table1_int4r t1 NORMALIZE table1_int4r t2
+		USING (a)
+		WITH (t, t)
+	) x;
+ a | b |   t    
+---+---+--------
+ a | B | [1,7)
+ b | B | [3,9)
+ c | G | [8,10)
+(3 rows)
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	table1_varcharr x NORMALIZE table1_varcharr y
+		ON TRUE
+		WITH (t, t)
+	) x;
+ a |      t      
+---+-------------
+ 0 | [A,ABC)
+ 0 | [ABC,BAA)
+ 0 | [BAA,BBB)
+ 0 | [BBB,BCD)
+ 0 | [BCD,C)
+ 0 | [C,D)
+ 1 | [C,D)
+ 1 | [D,X)
+ 0 | [ABC,BAA)
+ 0 | [BAA,BBB)
+ 0 | [BBB,BCD)
+ 0 | [xABC,xBCD)
+ 0 | [BAA,BBB)
+(13 rows)
+
+--
+-- TEMPORAL NORMALIZER: SELECTION PUSH-DOWN
+--
+-- VALID TIME columns are not safe to be pushed down, for the rest everything
+-- should work as usual.
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r NORMALIZE table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3;
+                                  QUERY PLAN                                  
+------------------------------------------------------------------------------
+ Subquery Scan on x
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (lower(table1_int4r.t))
+               ->  Nested Loop Left Join
+                     Join Filter: ((lower(table1_int4r.t)) <@ table2_int4r.t)
+                     ->  WindowAgg
+                           ->  Seq Scan on table2_int4r
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on table1_int4r
+                                 ->  Seq Scan on table1_int4r table1_int4r_1
+(13 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r NORMALIZE table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3 AND lower(t) > 3;
+                                  QUERY PLAN                                  
+------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: (lower(x.t) > 3)
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (lower(table1_int4r.t))
+               ->  Nested Loop Left Join
+                     Join Filter: ((lower(table1_int4r.t)) <@ table2_int4r.t)
+                     ->  WindowAgg
+                           ->  Seq Scan on table2_int4r
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on table1_int4r
+                                 ->  Seq Scan on table1_int4r table1_int4r_1
+(14 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r NORMALIZE table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3 OR lower(t) > 3;
+                                  QUERY PLAN                                  
+------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: ((x.c < 3) OR (lower(x.t) > 3))
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (lower(table1_int4r.t))
+               ->  Nested Loop Left Join
+                     Join Filter: ((lower(table1_int4r.t)) <@ table2_int4r.t)
+                     ->  WindowAgg
+                           ->  Seq Scan on table2_int4r
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on table1_int4r
+                                 ->  Seq Scan on table1_int4r table1_int4r_1
+(13 rows)
+
+--
+-- TEMPORAL NORMALIZER: DATA TYPES
+--
+-- Data types: Timestamps
+-- We use to_char here to be sure that we have the same output format on all
+-- platforms and locale configuration
+SELECT a, to_char(lower(t), 'YYYY-MM-DD') ts, to_char(upper(t), 'YYYY-MM-DD') te FROM (
+	table_tsrange t1 NORMALIZE table_tsrange t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+ a |     ts     |     te     
+---+------------+------------
+ 0 | 2000-01-01 | 2000-01-05
+ 0 | 2000-01-05 | 2000-01-10
+ 1 | 2000-01-05 | 2000-01-20
+(3 rows)
+
+-- Data types: Double precision
+SELECT a, t FROM (
+	table1_int4r0 t1 NORMALIZE table1_int4r0 t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+ a |          t          
+---+---------------------
+ 0 | [1,1.11109999)
+ 0 | [1.11109999,1.1111)
+ 1 | [1.11109999,2)
+(3 rows)
+
+-- Data types: Double precision with +/- infinity
+SELECT a, t FROM (
+	table1_int4r1 t1 NORMALIZE table1_int4r1 t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+ a |       t       
+---+---------------
+ 0 | [1,2)
+ 0 | [2,Infinity)
+ 1 | [-Infinity,2)
+(3 rows)
+
+--
+-- TEMPORAL ALIGNER AND NORMALIZER: VIEWS
+--
+-- Views with temporal normalization
+CREATE TEMP VIEW v AS SELECT * FROM (
+	table1_int4r NORMALIZE table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x;
+TABLE v;
+ a | b |   t    
+---+---+--------
+ a | B | [1,2)
+ a | B | [2,3)
+ a | B | [3,4)
+ a | B | [4,5)
+ a | B | [5,7)
+ b | B | [3,4)
+ b | B | [4,5)
+ b | B | [5,7)
+ b | B | [7,9)
+ c | G | [8,10)
+(10 rows)
+
+DROP VIEW v;
+-- Views with temporal alignment
+CREATE TEMP VIEW v AS SELECT * FROM (
+	table1_int4r ALIGN table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x;
+TABLE v;
+ a | b |   t    
+---+---+--------
+ a | B | [1,2)
+ a | B | [2,5)
+ a | B | [3,4)
+ a | B | [5,7)
+ b | B | [3,4)
+ b | B | [3,5)
+ b | B | [5,7)
+ b | B | [7,9)
+ c | G | [8,10)
+(9 rows)
+
+DROP VIEW v;
+-- Testing temporal normalization with ambiguous columns, i.e. columns that
+-- are used internally...
+CREATE TEMP VIEW v AS SELECT * FROM (
+	table1_int4r AS r(p1, p1_0, "p1_-1") NORMALIZE table2_int4r s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1", t)
+	) x;
+TABLE v;
+ p1 | p1_0 | p1_-1  
+----+------+--------
+ a  | B    | [1,2)
+ a  | B    | [2,3)
+ a  | B    | [3,4)
+ a  | B    | [4,5)
+ a  | B    | [5,7)
+ b  | B    | [3,4)
+ b  | B    | [4,5)
+ b  | B    | [5,7)
+ b  | B    | [7,9)
+ c  | G    | [8,10)
+(10 rows)
+
+DROP VIEW v;
+-- Testing temporal alignment with ambiguous columns, i.e. columns that
+-- are used internally...
+CREATE TEMP VIEW v AS SELECT * FROM (
+	table1_int4r AS r(p1, p1_0, p1_1) ALIGN table2_int4r s
+		ON r.p1_0 = s.d
+		WITH (p1_1,t)
+	) x;
+TABLE v;
+ p1 | p1_0 |  p1_1  
+----+------+--------
+ a  | B    | [1,2)
+ a  | B    | [2,5)
+ a  | B    | [3,4)
+ a  | B    | [5,7)
+ b  | B    | [3,4)
+ b  | B    | [3,5)
+ b  | B    | [5,7)
+ b  | B    | [7,9)
+ c  | G    | [8,10)
+(9 rows)
+
+DROP VIEW v;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index eefdeea..1649077 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 stats_ext temporal_primitives
 
 # rules cannot run concurrently with any test that creates a view
 test: rules psql_crosstab amutils
diff --git a/src/test/regress/sql/temporal_primitives.sql b/src/test/regress/sql/temporal_primitives.sql
new file mode 100644
index 0000000..9378fc0
--- /dev/null
+++ b/src/test/regress/sql/temporal_primitives.sql
@@ -0,0 +1,395 @@
+--
+-- TEMPORAL PRIMITIVES: ALIGN AND NORMALIZE
+--
+SET datestyle TO ymd;
+
+CREATE TYPE varcharrange AS RANGE (SUBTYPE=varchar);
+CREATE TYPE floatrange AS RANGE (SUBTYPE = float8, SUBTYPE_DIFF = float8mi);
+
+CREATE TEMP TABLE table1_int4r (a char, b char, t int4range);
+CREATE TEMP TABLE table2_int4r (c int, d char, t int4range);
+
+INSERT INTO table1_int4r VALUES
+('a','B','[1,7)'),
+('b','B','[3,9)'),
+('c','G','[8,10)');
+INSERT INTO table2_int4r VALUES
+(1,'B','[2,5)'),
+(2,'B','[3,4)'),
+(3,'B','[7,9)');
+
+-- VALID TIME columns (i.e., ts and te) are no longer at the end of the
+-- targetlist.
+CREATE TEMP TABLE table1_int4r_mix AS SELECT a, t, b FROM table1_int4r;
+CREATE TEMP TABLE table2_int4r_mix AS SELECT t, c, d FROM table2_int4r;
+
+-- VALID TIME columns as VARCHARs
+CREATE TEMP TABLE table1_varcharr (a int, t varcharrange);
+CREATE TEMP TABLE table2_varcharr (a int, t varcharrange);
+
+INSERT INTO table1_varcharr VALUES
+(0, varcharrange('A', 'D')),
+(1, varcharrange('C', 'X')),
+(0, varcharrange('ABC', 'BCD')),
+(0, varcharrange('xABC', 'xBCD')),
+(0, varcharrange('BAA', 'BBB'));
+
+INSERT INTO table2_varcharr VALUES
+(0, varcharrange('A', 'D')),
+(1, varcharrange('C', 'X'));
+
+-- Tables to check different data types, and corner cases
+CREATE TEMP TABLE table_tsrange (a int, t tsrange);
+CREATE TEMP TABLE table1_int4r0 (a int, t floatrange);
+CREATE TEMP TABLE table1_int4r1 AS TABLE table1_int4r0;
+
+INSERT INTO table_tsrange VALUES
+(0, '[2000-01-01, 2000-01-10)'),
+(1, '[2000-01-05, 2000-01-20)');
+
+INSERT INTO table1_int4r0 VALUES
+(0, floatrange(1.0, 1.1111)),
+(1, floatrange(1.11109999, 2.0));
+
+INSERT INTO table1_int4r1 VALUES
+(0, floatrange(1.0, 'Infinity')),
+(1, floatrange('-Infinity', 2.0));
+
+
+--
+-- TEMPORAL ALIGNER: BASICS
+--
+
+-- Equality qualifiers
+SELECT * FROM (
+	table1_int4r ALIGN table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x;
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+SELECT * FROM (
+	table1_int4r ALIGN table2_int4r
+		ON table1_int4r.b = table2_int4r.d
+		WITH (table1_int4r.t, table2_int4r.t)
+	) x;
+
+-- Alignment with aggregation
+-- NB: Targetlist of outer query is *not* A_STAR...
+SELECT a, COUNT(a) FROM (
+	table1_int4r ALIGN table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x
+	GROUP BY a ORDER BY a;
+
+-- Equality qualifiers
+-- Test column positions where ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT * FROM (
+	table1_int4r_mix ALIGN table2_int4r_mix
+		ON b = d
+		WITH (t, t)
+	) x;
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+-- Test column positions where ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT * FROM (
+	table1_int4r_mix ALIGN table2_int4r_mix
+		ON table1_int4r_mix.b = table2_int4r_mix.d
+		WITH (table1_int4r_mix.t, table2_int4r_mix.t)
+	) x;
+
+-- Alignment with aggregation where targetlist of outer query is *not* A_STAR...
+-- Test column positions where ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT a, COUNT(a) FROM (
+	table1_int4r_mix ALIGN table2_int4r_mix
+		ON b = d
+		WITH (t, t)
+	) x
+	GROUP BY a ORDER BY a;
+
+-- Test relations with differently named temporal bound attributes and relation
+-- and column aliases.
+SELECT * FROM (
+	table1_int4r ALIGN table2_int4r x(c,d,s)
+		ON b = d
+		WITH (t, s)
+	) x;
+
+
+--
+-- TEMPORAL ALIGNER: TEMPORAL JOIN EXAMPLE
+--
+
+-- Full temporal join example with absorbing where clause, timestamp
+-- propagation (see CTEs targetlists with V and U) and range types
+WITH t1 AS (SELECT *, t u FROM table1_int4r),
+	 t2 AS (SELECT c a, d b, t, t v FROM table2_int4r)
+SELECT t, b, x.a, y.a FROM (
+	t1 ALIGN t2
+		ON t1.b = t2.b
+		WITH (t, t)
+	) x
+	LEFT OUTER JOIN (
+		SELECT * FROM (
+		t2 ALIGN t1
+			ON t1.b = t2.b
+			WITH (t, t)
+		) y
+	) y
+	USING (b, t)
+	WHERE (
+			(lower(t) = lower(u) OR lower(t) = lower(v))
+			AND
+			(upper(t) = upper(u) OR upper(t) = upper(v))
+		)
+		OR u IS NULL
+		OR v IS NULL
+	ORDER BY 1,2,3,4;
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	table1_varcharr x ALIGN table1_varcharr y
+		ON TRUE
+		WITH (t, t)
+	) x;
+
+--
+-- TEMPORAL ALIGNER: SELECTION PUSH-DOWN
+--
+
+-- VALID TIME columns are not safe to be pushed down, for the rest everything
+-- should work as usual.
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r ALIGN table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r ALIGN table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3 AND lower(t) > 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r ALIGN table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3 OR lower(t) > 3;
+
+--
+-- TEMPORAL ALIGNER: DATA TYPES
+--
+
+-- Data types: Timestamps
+-- We use to_char here to be sure that we have the same output format on all
+-- platforms and locale configuration
+SELECT a, to_char(lower(t), 'YYYY-MM-DD') ts, to_char(upper(t), 'YYYY-MM-DD') te
+FROM (
+	table_tsrange t1 ALIGN table_tsrange t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+
+-- Data types: Double precision
+SELECT a, t FROM (
+	table1_int4r0 t1 ALIGN table1_int4r0 t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+
+-- Data types: Double precision with +/- infinity
+SELECT a, t FROM (
+	table1_int4r1 t1 ALIGN table1_int4r1 t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+
+
+--
+-- TEMPORAL NORMALIZER: BASICS
+--
+
+-- Equality qualifiers
+SELECT * FROM (
+	table1_int4r NORMALIZE table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x;
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+SELECT * FROM (
+	table1_int4r NORMALIZE table2_int4r
+		ON table1_int4r.b = table2_int4r.d
+		WITH (table1_int4r.t, table2_int4r.t)
+	) x;
+
+-- Alignment with aggregation
+-- NB: Targetlist of outer query is *not* A_STAR...
+SELECT a, COUNT(a) FROM (
+	table1_int4r NORMALIZE table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x
+	GROUP BY a ORDER BY a;
+
+-- Equality qualifiers
+-- Test column positions where ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT * FROM (
+	table1_int4r_mix NORMALIZE table2_int4r_mix
+		ON b = d
+		WITH (t, t)
+	) x;
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+-- Test column positions where t is not at the last column.
+-- Please note: This was a restriction in an early implementation.
+SELECT * FROM (
+	table1_int4r_mix NORMALIZE table2_int4r_mix
+		ON table1_int4r_mix.b = table2_int4r_mix.d
+		WITH (table1_int4r_mix.t, table2_int4r_mix.t)
+	) x;
+
+-- Alignment with aggregation where targetlist of outer query is *not* A_STAR...
+-- Test column positions where t is not at the last column.
+-- Please note: This was a restriction in an early implementation.
+SELECT a, COUNT(a) FROM (
+	table1_int4r_mix NORMALIZE table2_int4r_mix
+		ON b = d
+		WITH (t, t)
+	) x
+	GROUP BY a ORDER BY a;
+
+-- Test relations with differently named temporal bound attributes and relation
+-- and column aliases.
+SELECT * FROM (
+	table1_int4r NORMALIZE table2_int4r x(c,d,s)
+		ON b = d
+		WITH (t, s)
+	) x;
+
+-- Normalizer's USING clause (self-normalization)
+SELECT * FROM (
+	table1_int4r t1 NORMALIZE table1_int4r t2
+		USING (a)
+		WITH (t, t)
+	) x;
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	table1_varcharr x NORMALIZE table1_varcharr y
+		ON TRUE
+		WITH (t, t)
+	) x;
+
+
+--
+-- TEMPORAL NORMALIZER: SELECTION PUSH-DOWN
+--
+
+-- VALID TIME columns are not safe to be pushed down, for the rest everything
+-- should work as usual.
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r NORMALIZE table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r NORMALIZE table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3 AND lower(t) > 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	table2_int4r NORMALIZE table1_int4r
+		ON TRUE
+		WITH (t, t)
+	) x
+	WHERE c < 3 OR lower(t) > 3;
+
+--
+-- TEMPORAL NORMALIZER: DATA TYPES
+--
+
+-- Data types: Timestamps
+-- We use to_char here to be sure that we have the same output format on all
+-- platforms and locale configuration
+SELECT a, to_char(lower(t), 'YYYY-MM-DD') ts, to_char(upper(t), 'YYYY-MM-DD') te FROM (
+	table_tsrange t1 NORMALIZE table_tsrange t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+
+-- Data types: Double precision
+SELECT a, t FROM (
+	table1_int4r0 t1 NORMALIZE table1_int4r0 t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+
+-- Data types: Double precision with +/- infinity
+SELECT a, t FROM (
+	table1_int4r1 t1 NORMALIZE table1_int4r1 t2
+		ON t1.a = 0
+		WITH (t, t)
+	) x;
+
+--
+-- TEMPORAL ALIGNER AND NORMALIZER: VIEWS
+--
+
+-- Views with temporal normalization
+CREATE TEMP VIEW v AS SELECT * FROM (
+	table1_int4r NORMALIZE table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x;
+
+TABLE v;
+DROP VIEW v;
+
+-- Views with temporal alignment
+CREATE TEMP VIEW v AS SELECT * FROM (
+	table1_int4r ALIGN table2_int4r
+		ON b = d
+		WITH (t, t)
+	) x;
+
+TABLE v;
+DROP VIEW v;
+
+-- Testing temporal normalization with ambiguous columns, i.e. columns that
+-- are used internally...
+CREATE TEMP VIEW v AS SELECT * FROM (
+	table1_int4r AS r(p1, p1_0, "p1_-1") NORMALIZE table2_int4r s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1", t)
+	) x;
+
+TABLE v;
+DROP VIEW v;
+
+-- Testing temporal alignment with ambiguous columns, i.e. columns that
+-- are used internally...
+CREATE TEMP VIEW v AS SELECT * FROM (
+	table1_int4r AS r(p1, p1_0, p1_1) ALIGN table2_int4r s
+		ON r.p1_0 = s.d
+		WITH (p1_1,t)
+	) x;
+
+TABLE v;
+DROP VIEW v;
+
+
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to