[PROPOSAL] Temporal query processing with range types

Started by Anton Dignösover 9 years ago67 messages
#1Anton Dignös
dignoes@inf.unibz.it
1 attachment(s)

Hi hackers,

we are a group of researches that work on temporal databases. Our main focus is
the processing of data with time intervals, such as the range types in
PostgreSQL.

For this type of processing the range types that are stored with the tuples are
considered to represent the tuples' valid time, such as for instance the time
period an employee works for a project. Intuitively, the processing of temporal
operators corresponds to the use of "at each time point" in natural language.
For example, a temporal count on such a relation (temporal relation) shall
determine the count at each time point, while an anti-join shall compute an
anti-join at each time point.

We would like to contribute to PostgreSQL a solution that supports the query
processing of "at each time point". The basic idea is to offer two new
operators, NORMALIZE and ALIGN, whose purpose is to adjust (or split) the
ranges of tuples so that subsequent queries can use the usual grouping and
equality conditions to get the intended results.

Query processing works as follows:
<temporal operator> := <NORMALIZE | ALIGN> + <traditional operator>

Attached is a proposal patch file that implements this extension and on which
the example queries below can be run. The support includes distinct,
aggregation, set operations, Cartesian product, Join, Left Join, Right Join,
Full Join, and Anti-join.

In our prototype the syntax for the two new operators is:

... FROM (table_ref NORMALIZE table_ref
USING(att_list)
WITH (columnref, columnref)
) alias_clause ...

and

... FROM (table_ref ALIGN table_ref
ON a_expr
WITH (columnref, columnref)
) alias_clause ...

where NORMALIZE is used for distinct, aggregation and set operations and ALIGN
is used for Cartesian product, Join, Left Join, Right Join, Full Join, and
Anti-join.

-------------------------------------------------------------------------------
EXAMPLE FOR AGGREGATION
-------------------------------------------------------------------------------

Relation 'projects' records when employees are assigned to projects.

CREATE TABLE projects (empl VARCHAR, proj VARCHAR, pay FLOAT, t DATERANGE);
INSERT INTO projects VALUES
('Ann', 'P1', 60, DATERANGE('2016-01-01', '2016-09-01')),
('Sam', 'P1', 40, DATERANGE('2016-06-01', '2016-12-01')),
('Joe', 'P2', 80, DATERANGE('2016-03-01', '2016-06-01'));

TABLE projects;

empl | proj | pay | t
------+------+-----+-------------------------
Ann | P1 | 60 | [2016-01-01,2016-09-01)
Sam | P1 | 40 | [2016-06-01,2016-12-01)
Joe | P2 | 80 | [2016-03-01,2016-06-01)
(3 rows)

QUERY1: At each time point, what is the number of employees assigned
to projects?

First, we use NORMALIZE to adjust the ranges so that they can be
aggregated as usual by using grouping on the adjusted timestamp t:

SELECT *
FROM (projects p1 NORMALIZE projects p2 USING() WITH(t,t)) p_adjusted;

empl | proj | pay | t
------+------+-----+-------------------------
Ann | P1 | 60 | [2016-01-01,2016-03-01)
Ann | P1 | 60 | [2016-03-01,2016-06-01)
Ann | P1 | 60 | [2016-06-01,2016-09-01)
Sam | P1 | 40 | [2016-06-01,2016-09-01)
Sam | P1 | 40 | [2016-09-01,2016-12-01)
Joe | P2 | 80 | [2016-03-01,2016-06-01)
(6 rows)

Observe that the time ranges have been adjusted, and it is now trivial
to compute the count of employees at each time point by grouping on t:

SELECT COUNT(*), t
FROM (projects p1 NORMALIZE projects p2 USING() WITH(t,t)) p_adjusted
GROUP BY t;

count | t
-------+-------------------------
1 | [2016-01-01,2016-03-01)
2 | [2016-03-01,2016-06-01)
2 | [2016-06-01,2016-09-01)
1 | [2016-09-01,2016-12-01)
(4 rows)

QUERY2: At each time point, what is the number of employees assigned
to EACH project?

SELECT *
FROM (projects p1 NORMALIZE projects p2 USING(proj) WITH(t,t)) p_adjusted;

empl | proj | pay | t
------+------+-----+-------------------------
Ann | P1 | 60 | [2016-01-01,2016-06-01)
Ann | P1 | 60 | [2016-06-01,2016-09-01)
Sam | P1 | 40 | [2016-06-01,2016-09-01)
Sam | P1 | 40 | [2016-09-01,2016-12-01)
Joe | P2 | 80 | [2016-03-01,2016-06-01)
(5 rows)

and

SELECT COUNT(*), proj, t
FROM (projects p1 NORMALIZE projects p2 USING(proj) WITH(t,t)) p_adjusted
GROUP BY proj, t;

count | proj | t
-------+------+-------------------------
1 | P2 | [2016-03-01,2016-06-01)
1 | P1 | [2016-01-01,2016-06-01)
2 | P1 | [2016-06-01,2016-09-01)
1 | P1 | [2016-09-01,2016-12-01)
(4 rows)

-------------------------------------------------------------------------------
EXAMPLE FOR ANTI-JOIN
-------------------------------------------------------------------------------

QUERY3: At each time point, deterime the employees with the highest
salary.

Without time this query is answered with a NOT EXISTS subquery. With
time periods everything remains, but the time ranges must be adjusted
first:

SELECT *
FROM (projects p1 ALIGN projects p2 ON p1.pay < p2.pay WITH (t,t)) p1_adjusted
WHERE NOT EXISTS (
SELECT *
FROM (projects p2 ALIGN projects p1 ON p1.pay < p2.pay WITH (t,t)) p2_adjusted
WHERE p1_adjusted.pay < p2_adjusted.pay
AND p1_adjusted.t = p2_adjusted.t );

empl | proj | pay | t
------+------+-----+-------------------------
Ann | P1 | 60 | [2016-01-01,2016-03-01)
Ann | P1 | 60 | [2016-06-01,2016-09-01)
Sam | P1 | 40 | [2016-09-01,2016-12-01)
Joe | P2 | 80 | [2016-03-01,2016-06-01)
(4 rows)

-------------------------------------------------------------------------------
EXAMPLE FOR JOINS
-------------------------------------------------------------------------------

CREATE TABLE managers (mgr VARCHAR, proj VARCHAR, t DATERANGE);
INSERT INTO managers VALUES
('Tom', 'P1', DATERANGE('2016-09-01', '2016-12-01')),
('Frank', 'P2', DATERANGE('2016-01-01', '2016-12-01'));

TABLE managers;

mgr | proj | t
-------+------+-------------------------
Tom | P1 | [2016-09-01,2016-12-01)
Frank | P2 | [2016-01-01,2016-12-01)
(2 rows)

QUERY4: At each time point, determine employees and their managers
(including the periods with no employee or no manager).

WITH p_ext AS (SELECT *, t u FROM projects),
m_ext AS (SELECT *, t v FROM managers)
SELECT proj, empl, mgr, t
FROM (p_ext ALIGN m_ext ON m_ext.proj = p_ext.proj WITH (t,t)) p_adjusted
FULL OUTER JOIN
(m_ext ALIGN p_ext ON m_ext.proj = p_ext.proj WITH (t,t)) m_adjusted
USING (proj, t)
WHERE t = u * v OR v IS NULL OR u IS NULL;

proj | empl | mgr | t
------+------+-------+-------------------------
P1 | Ann | | [2016-01-01,2016-09-01)
P1 | Sam | | [2016-06-01,2016-09-01)
P1 | Sam | Tom | [2016-09-01,2016-12-01)
P2 | | Frank | [2016-01-01,2016-03-01)
P2 | Joe | Frank | [2016-03-01,2016-06-01)
P2 | | Frank | [2016-06-01,2016-12-01)
(6 rows)

-------------------------------------------------------------------------------
IMPLEMENTATION
-------------------------------------------------------------------------------

Our implementation approach is to use two operators that take care of
adjusting the ranges in such a way that afterwards the corresponding
nontemporal operator can be used to compute the result:
input --> {NORMALIZE/ALIGN} --> nontemporal operator --> result.

The two operators (NORMALIZE and ALIGN) in this prototype are rewritten into
subqueries, which then serve as input for a new executor function
ExecTemporalAdjustment. The executor function takes care of the splitting of
the range types.

For NORMALIZE the tuples' ranges need to be split into all sub-ranges
according to all matching ranges of the second relation. For this we
create a subquery that first joins one relation with the range
boundaries of the other and then sorts the result. The executor
function splits the ranges in a sweep-line based manner.

For ALIGN the tuples' ranges must be split into all intersections and
differences with the other relation according to the join condition.
For this we create a subquery that first joins the two relations and
then sorts the result. The executor function splits the ranges
accordingly in a sweep-line based manner.

After the splitting it is possible to only use equality (GROUP BY,
DISTINCT, JOIN CONDITION, ...) on the range types to get the intended
result.

The subquery is 'visible' with the EXPLAIN command.

Range types need to be of half-open, i.e., [lower, upper).

Unbound ranges (Infinity), NaN, and NULL values (of ranges and range
bounds) are not yet supported.

-------------------------------------------------------------------------------

We are grateful for any feedback.

Best regards,

Anton, Johann, Michael, Peter

Attachments:

tpg_primitives_out_v1.patchapplication/octet-stream; name=tpg_primitives_out_v1.patchDownload
diff --git src/backend/commands/explain.c src/backend/commands/explain.c
index dbd27e5..5e15658 100644
--- src/backend/commands/explain.c
+++ src/backend/commands/explain.c
@@ -868,6 +868,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 src/backend/executor/Makefile src/backend/executor/Makefile
index 51edd4c..42801d3 100644
--- src/backend/executor/Makefile
+++ src/backend/executor/Makefile
@@ -25,6 +25,8 @@ OBJS = execAmi.o execCurrent.o execGrouping.o execIndexing.o execJunk.o \
        nodeSamplescan.o nodeSeqscan.o nodeSetOp.o nodeSort.o nodeUnique.o \
        nodeValuesscan.o nodeCtescan.o nodeWorktablescan.o \
        nodeGroup.o nodeSubplan.o nodeSubqueryscan.o nodeTidscan.o \
-       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o
+       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o \
+       nodeTemporalAdjustment.o
+
 
 include $(top_srcdir)/src/backend/common.mk
diff --git src/backend/executor/execProcnode.c src/backend/executor/execProcnode.c
index 554244f..610d753 100644
--- src/backend/executor/execProcnode.c
+++ src/backend/executor/execProcnode.c
@@ -114,6 +114,7 @@
 #include "executor/nodeValuesscan.h"
 #include "executor/nodeWindowAgg.h"
 #include "executor/nodeWorktablescan.h"
+#include "executor/nodeTemporalAdjustment.h"
 #include "nodes/nodeFuncs.h"
 #include "miscadmin.h"
 
@@ -334,6 +335,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 */
@@ -531,6 +537,10 @@ ExecProcNode(PlanState *node)
 			result = ExecLimit((LimitState *) node);
 			break;
 
+		case T_TemporalAdjustmentState:
+			result = ExecTemporalAdjustment((TemporalAdjustmentState *) node);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
 			result = NULL;
@@ -779,6 +789,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;
@@ -812,3 +826,4 @@ ExecShutdownNode(PlanState *node)
 
 	return planstate_tree_walker(node, ExecShutdownNode, NULL);
 }
+
diff --git src/backend/executor/nodeTemporalAdjustment.c src/backend/executor/nodeTemporalAdjustment.c
new file mode 100644
index 0000000..c9e4029
--- /dev/null
+++ src/backend/executor/nodeTemporalAdjustment.c
@@ -0,0 +1,529 @@
+#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 */
+
+
+/*
+ * #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);
+}
+#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);
+}
+
+/*
+ * 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->attNumTs - 1] = newTs;
+	node->newValues[node->temporalCl->attNumTe - 1] = newTe;
+	if(node->temporalCl->attNumTr != -1)
+		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);
+
+#ifdef TEMPORAL_DEBUG
+	printf("Storing tuple:\n");
+	print_slot(slotToStoreIn);
+#endif
+}
+
+/*
+ * 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;
+}
+
+/*
+ * 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.ts, r.te, s.ts, s.te)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1, P2)
+ * 	  where r_1,...,r_n are all attributes from relation r. Two of these
+ * 	  attributes are temporal boundaries, namely TS and TE. The interval
+ * 	  [TS,TE) 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.ts, r.te, s.ts, s.te)) x
+ * 	  or
+ * 	  ... FROM (r NORMALIZE s ON theta WITH (r.ts, r.te, s.ts, s.te)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1)
+ * 	  where r_1,...,r_n are all attributes from relation r. Two of these
+ * 	  attributes are temporal boundaries, namely TS and TE. The interval
+ * 	  [TS,TE) represents the VALID TIME of each tuple. RN is the
+ * 	  temporal group ID or row number, and P1 is union of both
+ * 	  timepoints TS and TE of relation s.
+ */
+TupleTableSlot *
+ExecTemporalAdjustment(TemporalAdjustmentState *node)
+{
+	TupleTableSlot 		*out;
+	TupleTableSlot 		*curr;
+	TupleTableSlot 		*prev;
+	PlanState  			*outerPlan;
+	bool 				 produced;
+	bool 				 isNull;
+	Datum 				 currP1;
+	Datum 				 currP2;
+	Datum				 currRN;
+	Datum				 prevRN;
+	Datum				 prevTe;
+
+	outerPlan = outerPlanState(node);
+	out = node->ss.ps.ps_ResultTupleSlot;
+	prev = node->ss.ss_ScanTupleSlot;
+	curr = outerPlan->ps_ResultTupleSlot;
+
+	if(node->firstCall)
+	{
+		curr = ExecProcNode(outerPlan);
+		if(TupIsNull(curr))
+			return NULL;
+		ExecCopySlot(prev, curr);
+		node->sameleft = true;
+		node->firstCall = false;
+		node->outrn = 0;
+		node->sweepline = slotGetAttrNotNull(curr, node->temporalCl->attNumTs);
+	}
+
+#ifdef TEMPORAL_DEBUG
+	printf("CURR tuple:\n");
+	print_slot(curr);
+	printf("\tsweepline = %s\n",
+		   datumToString(curr->tts_tupleDescriptor->attrs[node->temporalCl->attNumTs - 1]->atttypid,
+						 node->sweepline));
+	printf("\tsameleft = %s\n", node->sameleft ? "t" : "f");
+	fflush(stdout);
+#endif
+
+	produced = false;
+	while(!produced && !TupIsNull(prev))
+	{
+		if(node->sameleft)
+		{
+			currRN = slotGetAttrNotNull(curr, node->temporalCl->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,  node->temporalCl->attNumP1, &isNull);
+			if (isNull)
+				node->sameleft = false;
+
+			if(!isNull && isLessThan(node->sweepline, currP1, node))
+			{
+				temporalAdjustmentStoreTuple(node, curr, out,
+											 node->sweepline, currP1);
+				produced = true;
+				node->sweepline = 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,
+												node->temporalCl->attNumP2);
+
+					/* If alignment check to not produce the same tuple again */
+					if(TupIsNull(out)
+						|| !isEqual(heapGetAttrNotNull(out, node->temporalCl->attNumTs),
+									currP1,
+									node)
+						|| !isEqual(heapGetAttrNotNull(out, node->temporalCl->attNumTe),
+									currP2,
+									node)
+						|| node->outrn != DatumGetInt64(currRN))
+					{
+						temporalAdjustmentStoreTuple(node, curr, out,
+													 currP1, currP2);
+
+						/* sweepline = max(sweepline, curr.P2) */
+						if (isLessThan(node->sweepline, currP2, node))
+							node->sweepline = currP2;
+
+						node->outrn = DatumGetInt64(currRN);
+						produced = true;
+					}
+				}
+
+				ExecCopySlot(prev, curr);
+				curr = ExecProcNode(outerPlan);
+
+				if(TupIsNull(curr))
+					node->sameleft = false;
+				else
+				{
+					currRN = slotGetAttrNotNull(curr,
+												node->temporalCl->attNumRN);
+					prevRN = slotGetAttrNotNull(prev,
+												node->temporalCl->attNumRN);
+					node->sameleft =
+							DatumGetInt64(currRN) == DatumGetInt64(prevRN);
+				}
+			}
+		}
+		else
+		{
+			prevTe = heapGetAttrNotNull(prev, node->temporalCl->attNumTe);
+
+			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, node->temporalCl->attNumRN);
+				node->outrn = DatumGetInt64(currRN);
+				produced = true;
+			}
+
+			if(TupIsNull(curr))
+				prev = ExecClearTuple(prev);
+			else
+			{
+				ExecCopySlot(prev, curr);
+				node->sweepline = slotGetAttrNotNull(curr,
+													 node->temporalCl->attNumTs);
+			}
+			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));
+
+	/* 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;
+
+	/*
+	 * Miscellaneous initialization
+	 *
+	 * Unique 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.
+	 */
+	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 align nodes do no projections, so initialize projection info for
+	 * this node appropriately
+	 */
+	ExecAssignResultTypeFromTL(&state->ss.ps);
+	state->ss.ps.ps_ProjInfo = NULL;
+	state->ss.ps.ps_TupFromTlist = false;
+
+	state->alignment = node->temporalCl->temporalType == TEMPORAL_TYPE_ALIGNER;
+	state->temporalCl = copyObject(node->temporalCl);
+	state->firstCall = true;
+
+	/*
+	 * Init masks
+	 */
+	state->nullMask = palloc0(sizeof(bool) * node->numCols);
+	state->tsteMask = palloc0(sizeof(bool) * node->numCols);
+
+	state->tsteMask[state->temporalCl->attNumTs - 1] = true;
+	state->tsteMask[state->temporalCl->attNumTe - 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);
+
+	/*
+	 * Range types in boundaries need special treatment:
+	 * - there is an extra column in each tuple that must be changed
+	 * - and a range constructor method that must be called
+	 */
+	if(node->temporalCl->attNumTr != -1)
+	{
+		state->tsteMask[state->temporalCl->attNumTr - 1] = true;
+		fmgr_info(fmgr_internal_function("range_constructor2"), rcFunctionInfo);
+		rcFunctionInfo->fn_expr = (fmNodePtr) node->rangeVar;
+		InitFunctionCallInfoData(state->rcFuncCallInfo, rcFunctionInfo, 2,
+								 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));
+}
+
+
+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 src/backend/nodes/copyfuncs.c src/backend/nodes/copyfuncs.c
index 3244c76..5b42f48 100644
--- src/backend/nodes/copyfuncs.c
+++ src/backend/nodes/copyfuncs.c
@@ -1932,6 +1932,9 @@ _copyJoinExpr(const JoinExpr *from)
 	COPY_NODE_FIELD(quals);
 	COPY_NODE_FIELD(alias);
 	COPY_SCALAR_FIELD(rtindex);
+	COPY_NODE_FIELD(temporalBounds);
+	COPY_SCALAR_FIELD(inTmpPrimTempType);
+	COPY_SCALAR_FIELD(inTmpPrimHasRangeT);
 
 	return newnode;
 }
@@ -2292,6 +2295,45 @@ _copyOnConflictClause(const OnConflictClause *from)
 	return newnode;
 }
 
+static TemporalClause *
+_copyTemporalClause(const TemporalClause *from)
+{
+	TemporalClause *newnode = makeNode(TemporalClause);
+
+	COPY_SCALAR_FIELD(temporalType);
+	COPY_SCALAR_FIELD(attNumTs);
+	COPY_SCALAR_FIELD(attNumTe);
+	COPY_SCALAR_FIELD(attNumTr);
+	COPY_SCALAR_FIELD(attNumP1);
+	COPY_SCALAR_FIELD(attNumP2);
+	COPY_SCALAR_FIELD(attNumRN);
+	COPY_STRING_FIELD(colnameTs);
+	COPY_STRING_FIELD(colnameTe);
+	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)
 {
@@ -2737,6 +2779,7 @@ _copyQuery(const Query *from)
 	COPY_NODE_FIELD(setOperations);
 	COPY_NODE_FIELD(constraintDeps);
 	COPY_NODE_FIELD(withCheckOptions);
+	COPY_NODE_FIELD(temporalClause);
 
 	return newnode;
 }
@@ -2804,6 +2847,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);
@@ -5065,6 +5109,12 @@ copyObject(const void *from)
 		case T_RoleSpec:
 			retval = _copyRoleSpec(from);
 			break;
+		case T_TemporalClause:
+			retval = _copyTemporalClause(from);
+			break;
+		case T_TemporalAdjustment:
+			retval = _copyTemporalAdjustment(from);
+			break;
 
 			/*
 			 * MISCELLANEOUS NODES
diff --git src/backend/nodes/equalfuncs.c src/backend/nodes/equalfuncs.c
index 1eb6799..3bf6a41 100644
--- src/backend/nodes/equalfuncs.c
+++ src/backend/nodes/equalfuncs.c
@@ -744,6 +744,9 @@ _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);
+	COMPARE_SCALAR_FIELD(inTmpPrimHasRangeT);
 
 	return true;
 }
diff --git src/backend/nodes/makefuncs.c src/backend/nodes/makefuncs.c
index d72a85e..cbe88a2 100644
--- src/backend/nodes/makefuncs.c
+++ 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 -
@@ -608,3 +691,4 @@ makeGroupingSet(GroupingSetKind kind, List *content, int location)
 	n->location = location;
 	return n;
 }
+
diff --git src/backend/nodes/outfuncs.c src/backend/nodes/outfuncs.c
index e196622..85d4d8d 100644
--- src/backend/nodes/outfuncs.c
+++ src/backend/nodes/outfuncs.c
@@ -1550,6 +1550,9 @@ _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);
+	WRITE_BOOL_FIELD(inTmpPrimHasRangeT);
 }
 
 static void
@@ -1818,6 +1821,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");
@@ -2497,6 +2512,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);
@@ -2562,6 +2578,38 @@ _outXmlSerialize(StringInfo str, const XmlSerialize *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(attNumTs);
+	WRITE_INT_FIELD(attNumTe);
+	WRITE_INT_FIELD(attNumTr);
+	WRITE_INT_FIELD(attNumP1);
+	WRITE_INT_FIELD(attNumP2);
+	WRITE_INT_FIELD(attNumRN);
+	WRITE_STRING_FIELD(colnameTs);
+	WRITE_STRING_FIELD(colnameTe);
+	WRITE_STRING_FIELD(colnameTr);
+}
+
+static void
 _outColumnDef(StringInfo str, const ColumnDef *node)
 {
 	WRITE_NODE_TYPE("COLUMNDEF");
@@ -2692,6 +2740,7 @@ _outQuery(StringInfo str, const Query *node)
 	WRITE_NODE_FIELD(rowMarks);
 	WRITE_NODE_FIELD(setOperations);
 	WRITE_NODE_FIELD(constraintDeps);
+	WRITE_NODE_FIELD(temporalClause);
 }
 
 static void
@@ -3608,6 +3657,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;
@@ -3705,6 +3757,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);
@@ -3846,7 +3904,6 @@ outNode(StringInfo str, const void *obj)
 				break;
 
 			default:
-
 				/*
 				 * This should be an ERROR, but it's too useful to be able to
 				 * dump structures that outNode only understands part of.
diff --git src/backend/nodes/print.c src/backend/nodes/print.c
index a1f2941..5d73c41 100644
--- src/backend/nodes/print.c
+++ src/backend/nodes/print.c
@@ -25,7 +25,7 @@
 #include "optimizer/clauses.h"
 #include "parser/parsetree.h"
 #include "utils/lsyscache.h"
-
+#include "parser/parse_node.h"
 
 /*
  * print
@@ -492,3 +492,18 @@ print_slot(TupleTableSlot *slot)
 
 	debugtup(slot, NULL);
 }
+
+/*
+ * print_namespace
+ * 		print out all name space items' RTEs.
+ */
+void
+print_namespace(const List *namespace)
+{
+	ListCell   *lc;
+	foreach(lc, namespace)
+	{
+		ParseNamespaceItem *nsitem = (ParseNamespaceItem *) lfirst(lc);
+		pprint(nsitem->p_rte);
+	}
+}
diff --git src/backend/nodes/readfuncs.c src/backend/nodes/readfuncs.c
index 94954dc..3bc0b45 100644
--- src/backend/nodes/readfuncs.c
+++ src/backend/nodes/readfuncs.c
@@ -261,6 +261,7 @@ _readQuery(void)
 	READ_NODE_FIELD(rowMarks);
 	READ_NODE_FIELD(setOperations);
 	READ_NODE_FIELD(constraintDeps);
+	READ_NODE_FIELD(temporalClause);
 
 	READ_DONE();
 }
@@ -422,6 +423,28 @@ _readSetOperationStmt(void)
 	READ_DONE();
 }
 
+/*
+ * _readTemporalClause
+ */
+static TemporalClause *
+_readTemporalClause(void)
+{
+	READ_LOCALS(TemporalClause);
+
+	READ_ENUM_FIELD(temporalType, TemporalType);
+	READ_INT_FIELD(attNumTs);
+	READ_INT_FIELD(attNumTe);
+	READ_INT_FIELD(attNumTr);
+	READ_INT_FIELD(attNumP1);
+	READ_INT_FIELD(attNumP2);
+	READ_INT_FIELD(attNumRN);
+	READ_STRING_FIELD(colnameTs);
+	READ_STRING_FIELD(colnameTe);
+	READ_STRING_FIELD(colnameTr);
+
+	READ_DONE();
+}
+
 
 /*
  *	Stuff from primnodes.h.
@@ -456,6 +479,17 @@ _readRangeVar(void)
 	READ_DONE();
 }
 
+static ColumnRef *
+_readColumnRef(void)
+{
+	READ_LOCALS(ColumnRef);
+
+	READ_NODE_FIELD(fields);
+	READ_LOCATION_FIELD(location);
+
+	READ_DONE();
+}
+
 static IntoClause *
 _readIntoClause(void)
 {
@@ -1221,6 +1255,9 @@ _readJoinExpr(void)
 	READ_NODE_FIELD(quals);
 	READ_NODE_FIELD(alias);
 	READ_INT_FIELD(rtindex);
+	READ_NODE_FIELD(temporalBounds);
+	READ_ENUM_FIELD(inTmpPrimTempType, TemporalType);
+	READ_BOOL_FIELD(inTmpPrimHasRangeT);
 
 	READ_DONE();
 }
@@ -2386,6 +2423,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 src/backend/optimizer/path/allpaths.c src/backend/optimizer/path/allpaths.c
index 88d833a..e226d33 100644
--- src/backend/optimizer/path/allpaths.c
+++ 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 */
@@ -127,6 +128,7 @@ static void subquery_push_qual(Query *subquery,
 static void recurse_push_qual(Node *setOp, Query *topquery,
 				  RangeTblEntry *rte, Index rti, Node *qual);
 static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
+static bool allWindowFuncsHaveRowId(List *targetList);
 
 
 /*
@@ -2323,7 +2325,8 @@ subquery_is_pushdown_safe(Query *subquery, Query *topquery,
 		return false;
 
 	/* Check points 3 and 4 */
-	if (subquery->distinctClause || subquery->hasWindowFuncs)
+	if (subquery->distinctClause || subquery->hasWindowFuncs
+			|| subquery->temporalClause)
 		safetyInfo->unsafeVolatile = true;
 
 	/*
@@ -2433,6 +2436,7 @@ static void
 check_output_expressions(Query *subquery, pushdown_safety_info *safetyInfo)
 {
 	ListCell   *lc;
+	bool wfsafe = allWindowFuncsHaveRowId(subquery->targetList);
 
 	foreach(lc, subquery->targetList)
 	{
@@ -2470,12 +2474,35 @@ 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 resnoTs =
+					((TemporalClause *)subquery->temporalClause)->attNumTs;
+			AttrNumber resnoTe =
+					((TemporalClause *)subquery->temporalClause)->attNumTe;
+			AttrNumber resnoRangeT =
+					((TemporalClause *)subquery->temporalClause)->attNumTr;
+
+			if (tle->resno == resnoTs
+					|| tle->resno == resnoTe
+					|| tle->resno == resnoRangeT)
+			{
+				safetyInfo->unsafeColumns[tle->resno] = true;
+				continue;
+			}
+		}
 	}
 }
 
@@ -2545,6 +2572,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
@@ -2828,6 +2881,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 src/backend/optimizer/plan/createplan.c src/backend/optimizer/plan/createplan.c
index 54d601f..a6608ac 100644
--- src/backend/optimizer/plan/createplan.c
+++ src/backend/optimizer/plan/createplan.c
@@ -113,6 +113,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,
@@ -270,6 +273,9 @@ static ModifyTable *make_modifytable(PlannerInfo *root,
 				 List *resultRelations, List *subplans,
 				 List *withCheckOptionLists, List *returningLists,
 				 List *rowMarks, OnConflictExpr *onconflict, int epqParam);
+static TemporalAdjustment *make_temporalAdjustment(Plan *lefttree,
+						TemporalClause *temporalClause,
+						List *sortClause);
 
 
 /*
@@ -463,6 +469,11 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
 											  (LimitPath *) best_path,
 											  flags);
 			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);
@@ -2246,6 +2257,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;
+}
+
 
 /*****************************************************************************
  *
@@ -4819,6 +4857,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 = 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 src/backend/optimizer/plan/planner.c src/backend/optimizer/plan/planner.c
index b265628..945fac2 100644
--- src/backend/optimizer/plan/planner.c
+++ src/backend/optimizer/plan/planner.c
@@ -1969,6 +1969,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
@@ -4334,7 +4348,6 @@ create_ordered_paths(PlannerInfo *root,
 	return ordered_rel;
 }
 
-
 /*
  * make_group_input_target
  *	  Generate appropriate PathTarget for initial input to grouping nodes.
diff --git src/backend/optimizer/plan/setrefs.c src/backend/optimizer/plan/setrefs.c
index 6ea46c4..2a847f6 100644
--- src/backend/optimizer/plan/setrefs.c
+++ src/backend/optimizer/plan/setrefs.c
@@ -613,6 +613,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 src/backend/optimizer/plan/subselect.c src/backend/optimizer/plan/subselect.c
index a46cc10..f9b1ca5 100644
--- src/backend/optimizer/plan/subselect.c
+++ src/backend/optimizer/plan/subselect.c
@@ -2667,6 +2667,7 @@ finalize_plan(PlannerInfo *root, Plan *plan, Bitmapset *valid_params,
 		case T_Gather:
 		case T_SetOp:
 		case T_Group:
+		case T_TemporalAdjustment:
 			break;
 
 		default:
diff --git src/backend/optimizer/util/pathnode.c src/backend/optimizer/util/pathnode.c
index ce7ad54..f408f12 100644
--- src/backend/optimizer/util/pathnode.c
+++ src/backend/optimizer/util/pathnode.c
@@ -2362,6 +2362,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 src/backend/parser/Makefile src/backend/parser/Makefile
index fdd8485..35c20fa 100644
--- src/backend/parser/Makefile
+++ src/backend/parser/Makefile
@@ -15,7 +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_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_relation.o parse_target.o parse_type.o parse_utilcmd.o scansup.o \
+      parse_temporal.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git src/backend/parser/analyze.c src/backend/parser/analyze.c
index 29c8c4e..2ea10ce 100644
--- src/backend/parser/analyze.c
+++ 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"
@@ -1113,6 +1114,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");
@@ -1186,6 +1190,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 src/backend/parser/gram.y src/backend/parser/gram.y
index edf4516..14c417c 100644
--- src/backend/parser/gram.y
+++ src/backend/parser/gram.y
@@ -400,6 +400,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
@@ -452,11 +453,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
@@ -539,6 +543,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				opt_frame_clause frame_extent frame_bound
 %type <str>		opt_existing_window_name
 %type <boolean> opt_if_not_exists
+%type <list>    temporal_bounds_list
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -563,7 +568,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 ATTRIBUTE AUTHORIZATION
 
 	BACKWARD BEFORE BEGIN_P BETWEEN BIGINT BINARY BIT
@@ -608,7 +613,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 NEXT NO NONE
+	NAME_P NAMES NATIONAL NATURAL NCHAR NEXT NO NONE NORMALIZE
 	NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF
 	NULLS_P NUMERIC
 
@@ -10450,6 +10455,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.
@@ -10701,6 +10719,94 @@ 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 period boundaries with 4 comparable values
+					 * or two range typed values,
+					 * i.e. [lts, lte) is the left argument period, and
+					 *		[rts, rte) is the right argument period,
+					 * or two compatible range types with bounds like '[)'
+					 */
+					if(list_length($6) == 4 || list_length($6) == 2)
+						n->temporalBounds = $6;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have 2 range typed values, or four " \
+										"single 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 period boundaries with 4 comparable values
+					 * or two range typed values,
+					 * i.e. [lts, lte) is the left argument period, and
+					 *      [rts, rte) is the right argument period,
+					 * or two compatible range types with bounds like '[)'
+					 */
+					if(list_length($5) == 4 || list_length($5) == 2)
+						n->temporalBounds = $5;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have 2 range typed values, or four " \
+										"single values."),
+								 parser_errposition(@5)));
+
+					$$ = n;
+				}
 		;
 
 
@@ -14106,7 +14212,8 @@ type_func_name_keyword:
  * forced to.
  */
 reserved_keyword:
-			  ALL
+			  ALIGN
+			| ALL
 			| ANALYSE
 			| ANALYZE
 			| AND
@@ -14154,6 +14261,7 @@ reserved_keyword:
 			| LIMIT
 			| LOCALTIME
 			| LOCALTIMESTAMP
+			| NORMALIZE
 			| NOT
 			| NULL_P
 			| OFFSET
diff --git src/backend/parser/parse_clause.c src/backend/parser/parse_clause.c
index 751de4b..47d3391 100644
--- src/backend/parser/parse_clause.c
+++ src/backend/parser/parse_clause.c
@@ -39,6 +39,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"
@@ -967,6 +968,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.
 		 */
@@ -1029,6 +1067,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 src/backend/parser/parse_temporal.c src/backend/parser/parse_temporal.c
new file mode 100644
index 0000000..aa5a78c
--- /dev/null
+++ src/backend/parser/parse_temporal.c
@@ -0,0 +1,1546 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 can have four elements in a boundary
+ * list (i.e., WITH-clause of a temporal primitive) when we have two argument
+ * relations with scalar boundaries, or two entries if we have 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_LARGTST = 0,
+	TPB_LARGTE,
+	TPB_RARGTST,
+	TPB_RARGTE
+} 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,
+						  bool *hasRangeTypes,
+						  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,
+						   bool hasRangeTypes,
+						   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. These are either
+ * 		two scalar values for TS and TE, or a single range type value T holding
+ * 		both bounds.
+ */
+static List *
+temporalBoundGetLeftBounds(List *bounds)
+{
+	switch(list_length(bounds))
+	{
+		case 2: return list_make1(linitial(bounds));
+		case 4: return list_make2(linitial(bounds), lsecond(bounds));
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT),
+			 errmsg("Invalid temporal bound list length."),
+			 errhint("Specify four scalar columns for the " \
+					 "temporal boundaries, or two range-typed "\
+					 "columns.")));
+
+	/* Keep compiler quiet */
+	return NIL;
+}
+
+/*
+ * temporalBoundGetRightBounds -
+ * 		Return the right boundaries of a temporal bounds list. These are either
+ * 		two scalar values for TS and TE, or a single range type value T holding
+ * 		both bounds.
+ */
+static List *
+temporalBoundGetRightBounds(List *bounds)
+{
+	switch(list_length(bounds))
+	{
+		case 2: return list_make1(lsecond(bounds));
+		case 4: return list_make2(lthird(bounds), lfourth(bounds));
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT),
+			 errmsg("Invalid temporal bound list length."),
+			 errhint("Specify four scalar columns for the " \
+					 "temporal boundaries, or two range-typed "\
+					 "columns.")));
+
+	/* Keep compiler quiet */
+	return NIL;
+}
+
+/*
+ * 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)
+	{
+		/*
+		 * Four boundary entries means that we have 2x two scalar boundaries.
+		 * Which means the first two entries are start and end of the first
+		 * bound, and the 3th and 4th entry are start and end of the second
+		 * bound.
+		 */
+		case 4:
+			return list_nth(bounds, id);
+
+		/*
+		 * 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_LARGTST)
+				return linitial(bounds);
+			if(id == TPB_RARGTST || id == TPB_LARGTE)
+				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_LARGTST)
+				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 four scalar columns for the " \
+						 "temporal boundaries, or two range-typed "\
+						 "columns.")));
+
+	return NULL;
+}
+
+/*
+ * transformTemporalClause -
+ * 		If we have a temporal primitive query, we must find all attribute
+ * 		numbers for p1, p2, rn, ts, te, 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, all attribute numbers for temporal boundaries are unknown */
+	tc->attNumTr = -1;
+	tc->attNumTe = -1;
+	tc->attNumTs = -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;
+	}
+
+	/*
+	 * If we have range types the subquery splits it into separate
+	 * columns, called ts and te which are in between the p1- and
+	 * rn-column.
+	 */
+	if(tc->colnameTr)
+	{
+		tc->attNumTe = pos--;
+		tc->attNumTs = pos--;
+	}
+
+	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 if(strcmp(key, "ts") == 0)
+		{
+			tc->colnameTs = pstrdup(value);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumTs);
+			foundTsTe = true;
+		}
+		else if(strcmp(key, "te") == 0)
+		{
+			tc->colnameTe = pstrdup(value);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumTe);
+			foundTsTe = true;
+		}
+		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;
+
+		/* Temporal boundary is a range type */
+		if (tc->colnameTr)
+		{
+			if (strcmp(tle->resname, tc->colnameTr) == 0)
+				tc->attNumTr = tle->resno;
+		}
+		else /* Two scalar columns for boundaries */
+		{
+			if (strcmp(tle->resname, tc->colnameTs) == 0)
+				tc->attNumTs = tle->resno;
+			else if (strcmp(tle->resname, tc->colnameTe) == 0)
+				tc->attNumTe = tle->resno;
+		}
+	}
+
+	/* We need column attribute numbers for all temporal boundaries */
+	if(tc->attNumTs == -1
+			|| tc->attNumTe == -1
+			|| (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;
+
+	/* We use range types, remove splitted columns, i.e. upper/lower bounds */
+	if(tc->colnameTr) {
+		get_tle_by_resno(qry->targetList, tc->attNumTs)->resjunk = true;
+		get_tle_by_resno(qry->targetList, tc->attNumTe)->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, bool *hasRangeTypes, 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;
+
+	/*
+	 * We can have 2 or 4 column references, i.e. if we have 4, the first two
+	 * form the left argument period [largTs, largTe), and the last two the
+	 * right argument period respectively. Otherwise, we have two range typed
+	 * values of the form '[)' where the first argument contains the boundaries
+	 * of the left-hand-side, and the second argument contains the boundaries
+	 * of the RHS respectively. The parser checked already if there was another
+	 * number of arguments (not equal to 2 or 4) given.
+	 */
+	*hasRangeTypes = list_length(j->temporalBounds) == 2;
+
+	/*
+	 * 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);
+	if(*hasRangeTypes)
+	{
+		/*
+		 * Hardcoded column names for ts and te. We handle ambiguous column
+		 * names during the transformation of temporal primitive clauses.
+		 */
+		ssResult->temporalClause->colnameTs = "ts";
+		ssResult->temporalClause->colnameTe = "te";
+		ssResult->temporalClause->colnameTr =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTST);
+	}
+	else
+	{
+		ssResult->temporalClause->colnameTs =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTST);
+		ssResult->temporalClause->colnameTe =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTE);
+		ssResult->temporalClause->colnameTr = NULL;
+	}
+
+	/*
+	 * 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;
+	joinExpr->inTmpPrimHasRangeT = *hasRangeTypes;
+
+	return ssResult;
+}
+
+/*
+ * transformTemporalAligner -
+ * 		transform a TEMPORAL ALIGN clause into standard SQL
+ *
+ * INPUT:
+ * 		(r ALIGN s ON q WITH (r.ts, r.te, s.ts, s.te)) c
+ * 		where q can be any join qualifier, and r.ts, r.te, s.ts, and s.te
+ * 		can be any column name.
+ *
+ * OUTPUT:
+ * 		(
+ * 		SELECT r.*, GREATEST(r.ts, s.ts) P1, LEAST(r.te, s.te) P2
+ *      FROM
+ *      (
+ *      	SELECT *, row_id() OVER () rn FROM r
+ *      ) r
+ *      LEFT OUTER JOIN
+ *      s
+ *      ON q AND r.ts < s.te AND r.te > s.ts
+ *      ORDER BY rn, P1, P2
+ *      ) c
+ */
+Node *
+transformTemporalAligner(ParseState *pstate, JoinExpr *j)
+{
+	const int 	 		 UNKNOWN_LOCATION = -1;
+
+	bool				 hasRangeTypes;
+	SelectStmt			*ssResult;
+	RangeSubselect 		*rssResult;
+	ResTarget			*rtGreatest;
+	ResTarget			*rtLeast;
+	ResTarget			*rtLowerLarg;
+	ResTarget			*rtUpperLarg;
+	ColumnRef 			*crLargTs;
+	ColumnRef 			*crRargTs;
+	ColumnRef 			*crLargTe;
+	ColumnRef 			*crRargTe;
+	MinMaxExpr			*mmeGreatest;
+	MinMaxExpr			*mmeLeast;
+	FuncCall			*fcLowerLarg;
+	FuncCall			*fcLowerRarg;
+	FuncCall			*fcUpperLarg;
+	FuncCall			*fcUpperRarg;
+	List				*mmeGreatestArgs;
+	List				*mmeLeastArgs;
+	List				*boundariesExpr;
+	JoinExpr			*joinExpr;
+	A_Expr				*lowerBoundExpr;
+	A_Expr				*upperBoundExpr;
+	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, &hasRangeTypes,
+										 &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.
+	 */
+	if (hasRangeTypes)
+	{
+		crLargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crRargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   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);
+
+		rtLowerLarg = makeResTarget((Node *) fcLowerLarg,
+									ssResult->temporalClause->colnameTs);
+		rtUpperLarg = makeResTarget((Node *) fcUpperLarg,
+									ssResult->temporalClause->colnameTe);
+
+		ssResult->targetList = list_concat(ssResult->targetList,
+										   list_make2(rtLowerLarg, rtUpperLarg));
+	}
+	else
+	{
+		crLargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crLargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTE,
+										   largAlias->aliasname);
+		crRargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+		crRargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTE,
+										   rargAlias->aliasname);
+
+		/* Create argument list for function call to "greatest" and "least" */
+		mmeGreatestArgs = list_make2(crLargTs, crRargTs);
+		mmeLeastArgs = list_make2(crLargTe, crRargTe);
+
+		/*
+		 * Build Boolean expressions, i.e. "r.ts < s.te AND r.te > s.ts"
+		 * and concatenate it with q (=theta)
+		 */
+		lowerBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  "<",
+										  copyObject(crLargTs),
+										  copyObject(crRargTe),
+										  UNKNOWN_LOCATION);
+		upperBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  ">",
+										  copyObject(crLargTe),
+										  copyObject(crRargTs),
+										  UNKNOWN_LOCATION);
+
+		boundariesExpr = list_make2(lowerBoundExpr, upperBoundExpr);
+	}
+
+	/* 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.ts, r.te, s.ts, s.te)) c
+ *
+ * 		-- or --
+ *
+ * 		(r NORMALIZE s USING(atts) WITH (r.ts, r.te, s.ts, s.te)) c
+ * 		where q can be any join qualifier and r.ts, r.te, s.ts, and s.te
+ * 		can be any column name.
+ *
+ * OUTPUT:
+ * 		(
+ * 			SELECT r.*,
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT s.*, ts P1 FROM s
+ *      		UNION ALL
+ *      		SELECT s.*, te P1 FROM s
+ *      	) s
+ *      	ON q AND P1 >= r.ts AND P1 < r.te
+ *      	ORDER BY rn, P1
+ *      ) c
+ *
+ *      -- or --
+ *
+ * 		(
+ * 			SELECT r.*,
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT atts, ts P1 FROM s
+ *      		UNION
+ *      		SELECT atts, te P1 FROM s
+ *      	) s
+ *      	ON r.atts = s.atts AND P1 >= r.ts AND P1 < r.te
+ *      	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;
+	ResTarget			*rtLowerLarg;
+	ResTarget			*rtUpperLarg;
+	ColumnRef 			*crRargStar;
+	ColumnRef 			*crLargTsT = NULL;
+	ColumnRef 			*crRargTsT = NULL;
+	ColumnRef 			*crLargTe = NULL;
+	ColumnRef 			*crRargTe = NULL;
+	ColumnRef 			*crP1;
+	JoinExpr			*joinExpr;
+	A_Expr				*lowerBoundExpr;
+	A_Expr				*upperBoundExpr;
+	A_Expr				*containsExpr;
+	Node				*boolExpr;
+	Alias				*largAlias;
+	Alias				*rargAlias;
+	char 				*colnameRN;
+	char 				*colnameP1;
+	bool				 hasRangeTypes;
+	FuncCall			*fcLowerLarg = NULL;
+	FuncCall			*fcUpperLarg = NULL;
+	FuncCall			*fcLowerRarg = NULL;
+	FuncCall			*fcUpperRarg = NULL;
+	List				*boundariesExpr;
+
+	/* Create a select statement skeleton to be filled here */
+	ssResult = makeTemporalQuerySkeleton(j, &colnameRN, &colnameP1,
+										 NULL, &hasRangeTypes,
+										 &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. If we need only two range types
+	 * only Ts columnrefs are used.
+	 */
+	if (hasRangeTypes)
+	{
+		crLargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crRargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+
+		/* Create argument list for function call to "greatest" and "least" */
+		fcLowerLarg = makeFuncCall(SystemFuncName("lower"),
+								   list_make1(crLargTsT),
+								   UNKNOWN_LOCATION);
+		fcLowerRarg = makeFuncCall(SystemFuncName("lower"),
+								   list_make1(crRargTsT),
+								   UNKNOWN_LOCATION);
+		fcUpperLarg = makeFuncCall(SystemFuncName("upper"),
+								   list_make1(crLargTsT),
+								   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);
+
+		rtLowerLarg = makeResTarget((Node *) fcLowerLarg,
+									ssResult->temporalClause->colnameTs);
+		rtUpperLarg = makeResTarget((Node *) fcUpperLarg,
+									ssResult->temporalClause->colnameTe);
+
+		ssResult->targetList = list_concat(ssResult->targetList,
+										   list_make2(rtLowerLarg, rtUpperLarg));
+		/*
+		 * 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);
+	}
+	else
+	{
+		/*
+		 * Build column references, for use later.
+		 */
+		crLargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crLargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTE,
+										   largAlias->aliasname);
+		crRargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+		crRargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTE,
+										   rargAlias->aliasname);
+
+		/* Build resource target "ts P1" and "te P1" */
+		rtTsP1 = makeResTarget((Node *) crRargTsT, colnameP1);
+		rtTeP1 = makeResTarget((Node *) crRargTe, colnameP1);
+		/*
+		 * Build "contains" expressions, i.e. "P1 >= ts AND P1 < te"
+		 * and concatenate it with q (=theta)
+		 */
+		lowerBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  ">=",
+										  copyObject(crP1),
+										  copyObject(crLargTsT),
+										  UNKNOWN_LOCATION);
+		upperBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  "<",
+										  copyObject(crP1),
+										  copyObject(crLargTe),
+										  UNKNOWN_LOCATION);
+
+		boundariesExpr = list_make2(lowerBoundExpr, upperBoundExpr);
+	}
+
+	/*
+	 * Build "SELECT s.*, ts P1 FROM s" and "SELECT s.*, te P1 FROM s", iff we
+	 * have a ON-clause.
+	 * If we have an USING-clause with a name-list 'atts' build "SELECT atts,
+	 * ts P1 FROM s" and "SELECT atts, te 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	   *colnameTs = ssResult->temporalClause->colnameTs;
+		char	   *colnameTe = ssResult->temporalClause->colnameTe;
+		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(strcmp(usingItemName, colnameTs) == 0
+					|| strcmp(usingItemName, colnameTe) == 0
+					|| (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). These column
+ * 		names also differ depending on weither we have range types or scalars
+ * 		for temporal bounds. The list is then compared with the aliases from
+ * 		the current parser state, and renamed if necessary.
+ */
+static List *
+internalUseOnlyColumnNames(ParseState *pstate,
+						   bool hasRangeTypes,
+						   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"));
+
+	/* We split range types into upper and lower bounds, called ts and te */
+	if(hasRangeTypes)
+	{
+		filter = lappend(filter, makeString("ts"));
+		filter = lappend(filter, makeString("te"));
+	}
+
+	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. This means, if
+ * 		we have only one item in boundary list, all bounds must be range types.
+ * 		Otherwise, all bounds must be scalars.
+ */
+static void
+temporalBoundCheckIntegrity(ParseState *pstate,
+							 List *bounds,
+							 List *colnames,
+							 List *colvars,
+							 TemporalType tmpType)
+{
+	ListCell 	*lcNames;
+	ListCell 	*lcVars;
+	ListCell 	*lcBound;
+	ListCell 	*lcFilter;
+	bool 		 hasRangeTypes = list_length(bounds) == 1;
+	List		*filter = internalUseOnlyColumnNames(pstate,
+													 hasRangeTypes,
+													 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(hasRangeTypes && type->typtype != TYPTYPE_RANGE)
+					msg = "Invalid column type \"%s\" for the temporal bound " \
+						  "\"%s\". It must be a range type column.";
+
+				if(! hasRangeTypes && type->typtype == TYPTYPE_RANGE)
+					msg = "Invalid column type \"%s\" for the temporal bound " \
+						  "\"%s\". It must be a scalar type column (i.e., " \
+						  "not a range type).";
+
+				if (strlen(msg) > 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+							 errmsg(msg,
+									NameStr(type->typname),
+									NameListToString(crb->fields)),
+							 errhint("Specify four scalar columns for the " \
+									 "temporal boundaries, or 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;
+	bool 		foundTS = false;
+	bool 		foundTE = false;
+	int 		counterP1 = -1;
+	int 		counterP2 = -1;
+	int 		counterRN = -1;
+	int 		counterTS = -1;
+	int 		counterTE = -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);
+
+		if (j->inTmpPrimHasRangeT)
+		{
+			getColumnCounter(colname, "ts", &foundTS, &counterTS);
+			getColumnCounter(colname, "te", &foundTE, &counterTE);
+		}
+	}
+
+	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 (j->inTmpPrimHasRangeT)
+		{
+			getColumnCounter(colname, "ts", &foundTS, &counterTS);
+			getColumnCounter(colname, "te", &foundTE, &counterTE);
+		}
+	}
+
+	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;
+			A_Expr *e2;
+			List *qualArgs;
+			bool hasRangeTypes = list_length(j->temporalBounds) == 2;
+
+			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(! hasRangeTypes)
+			{
+				e2 = (A_Expr *) lsecond(qualArgs);
+				linitial(((ColumnRef *)e2->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);
+
+	if (foundTS)
+		addTemporalAlias(pstate, "ts", counterTS);
+
+	if (foundTE)
+		addTemporalAlias(pstate, "te", counterTE);
+
+	temporalBoundCheckIntegrity(pstate,
+								temporalBoundGetLeftBounds(j->temporalBounds),
+								l_colnames, l_colvars, j->inTmpPrimTempType);
+
+
+	temporalBoundCheckIntegrity(pstate,
+								temporalBoundGetRightBounds(j->temporalBounds),
+								r_colnames, r_colvars, j->inTmpPrimTempType);
+
+}
+
+
diff --git src/backend/utils/adt/windowfuncs.c src/backend/utils/adt/windowfuncs.c
index 3c1d3cf..e22814b 100644
--- src/backend/utils/adt/windowfuncs.c
+++ 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 src/backend/utils/errcodes.txt src/backend/utils/errcodes.txt
index be924d5..5d755a2 100644
--- src/backend/utils/errcodes.txt
+++ src/backend/utils/errcodes.txt
@@ -204,6 +204,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 src/include/catalog/pg_proc.h src/include/catalog/pg_proc.h
index 5d233e3..92d3d14 100644
--- src/include/catalog/pg_proc.h
+++ src/include/catalog/pg_proc.h
@@ -4965,6 +4965,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 = 3999 (  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 src/include/executor/nodeTemporalAdjustment.h src/include/executor/nodeTemporalAdjustment.h
new file mode 100644
index 0000000..7a4be3d
--- /dev/null
+++ 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 src/include/nodes/execnodes.h src/include/nodes/execnodes.h
index e7fd7bd..4e7845b 100644
--- src/include/nodes/execnodes.h
+++ src/include/nodes/execnodes.h
@@ -1252,6 +1252,29 @@ 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 */
+} TemporalAdjustmentState;
+
+/* ----------------
  *	 SeqScanState information
  * ----------------
  */
diff --git src/include/nodes/makefuncs.h src/include/nodes/makefuncs.h
index 01c5cb4..ac5e514 100644
--- src/include/nodes/makefuncs.h
+++ src/include/nodes/makefuncs.h
@@ -85,5 +85,9 @@ extern DefElem *makeDefElemExtended(char *nameSpace, char *name, Node *arg,
 					DefElemAction defaction);
 
 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 src/include/nodes/nodes.h src/include/nodes/nodes.h
index 6b850e4..d376658 100644
--- src/include/nodes/nodes.h
+++ src/include/nodes/nodes.h
@@ -79,6 +79,7 @@ typedef enum NodeTag
 	T_SetOp,
 	T_LockRows,
 	T_Limit,
+	T_TemporalAdjustment,
 	/* these aren't subclasses of Plan: */
 	T_NestLoopParam,
 	T_PlanRowMark,
@@ -127,6 +128,7 @@ typedef enum NodeTag
 	T_SetOpState,
 	T_LockRowsState,
 	T_LimitState,
+	T_TemporalAdjustmentState,
 
 	/*
 	 * TAGS FOR PRIMITIVE NODES (primnodes.h)
@@ -256,6 +258,7 @@ typedef enum NodeTag
 	T_LockRowsPath,
 	T_ModifyTablePath,
 	T_LimitPath,
+	T_TemporalAdjustmentPath,
 	/* these aren't subclasses of Path: */
 	T_EquivalenceClass,
 	T_EquivalenceMember,
@@ -452,6 +455,7 @@ typedef enum NodeTag
 	T_OnConflictClause,
 	T_CommonTableExpr,
 	T_RoleSpec,
+	T_TemporalClause,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
@@ -653,11 +657,19 @@ 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.
 	 */
+
 } JoinType;
 
 /*
diff --git src/include/nodes/parsenodes.h src/include/nodes/parsenodes.h
index 3773dd9..71eed0e 100644
--- src/include/nodes/parsenodes.h
+++ src/include/nodes/parsenodes.h
@@ -161,6 +161,8 @@ typedef struct Query
 										 * are only added during rewrite and
 										 * therefore are not written out as
 										 * part of Query. */
+
+	Node	   *temporalClause; /* temporal primitive node */
 } Query;
 
 
@@ -1312,6 +1314,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 src/include/nodes/plannodes.h src/include/nodes/plannodes.h
index 369179f..c638303 100644
--- src/include/nodes/plannodes.h
+++ src/include/nodes/plannodes.h
@@ -200,6 +200,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 src/include/nodes/primnodes.h src/include/nodes/primnodes.h
index 057cc2c..d19f9fb 100644
--- src/include/nodes/primnodes.h
+++ src/include/nodes/primnodes.h
@@ -58,6 +58,35 @@ 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   attNumTs;
+	AttrNumber   attNumTe;
+	AttrNumber   attNumTr;
+	AttrNumber   attNumP1;
+	AttrNumber   attNumP2;
+	AttrNumber   attNumRN;
+
+	char        *colnameTs;
+	char        *colnameTe;
+	char		*colnameTr;	    /* If range type used for bounds, or NULL */
+} TemporalClause;
+
 /*
  * RangeVar - range variable, used in FROM clauses
  *
@@ -1375,6 +1404,10 @@ 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 */
+	bool		 inTmpPrimHasRangeT; /* true, if bounds are range types */
 } JoinExpr;
 
 /*----------
diff --git src/include/nodes/print.h src/include/nodes/print.h
index 431d72d..2f875c0 100644
--- src/include/nodes/print.h
+++ 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 src/include/nodes/relation.h src/include/nodes/relation.h
index 2be8908..efbd03e 100644
--- src/include/nodes/relation.h
+++ src/include/nodes/relation.h
@@ -1045,6 +1045,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 src/include/optimizer/pathnode.h src/include/optimizer/pathnode.h
index 71d9154..17094f9 100644
--- src/include/optimizer/pathnode.h
+++ src/include/optimizer/pathnode.h
@@ -149,6 +149,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 src/include/parser/kwlist.h src/include/parser/kwlist.h
index 17ffef5..89b6022 100644
--- src/include/parser/kwlist.h
+++ 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)
@@ -254,6 +255,7 @@ PG_KEYWORD("nchar", NCHAR, COL_NAME_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 src/include/parser/parse_node.h src/include/parser/parse_node.h
index e3e359c..8817923 100644
--- src/include/parser/parse_node.h
+++ src/include/parser/parse_node.h
@@ -158,6 +158,12 @@ struct ParseState
 	RangeTblEntry *p_target_rangetblentry;
 
 	/*
+	 * 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 src/include/parser/parse_temporal.h src/include/parser/parse_temporal.h
new file mode 100644
index 0000000..3faffa7
--- /dev/null
+++ src/include/parser/parse_temporal.h
@@ -0,0 +1,48 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 void
+tpprint(const void *obj, const char *marker);
+
+#endif   /* PARSE_TEMPORAL_H */
diff --git src/include/utils/builtins.h src/include/utils/builtins.h
index 8cebc86..2f99558 100644
--- src/include/utils/builtins.h
+++ src/include/utils/builtins.h
@@ -1237,6 +1237,7 @@ extern Datum uuid_hash(PG_FUNCTION_ARGS);
 
 /* windowfuncs.c */
 extern Datum window_row_number(PG_FUNCTION_ARGS);
+extern Datum window_row_id(PG_FUNCTION_ARGS);
 extern Datum window_rank(PG_FUNCTION_ARGS);
 extern Datum window_dense_rank(PG_FUNCTION_ARGS);
 extern Datum window_percent_rank(PG_FUNCTION_ARGS);
#2David Fetter
david@fetter.org
In reply to: Anton Dignös (#1)
Re: [PROPOSAL] Temporal query processing with range types

On Fri, Jul 22, 2016 at 01:15:17PM +0200, Anton Dign�s wrote:

Hi hackers,

we are a group of researches that work on temporal databases. Our
main focus is the processing of data with time intervals, such as
the range types in PostgreSQL.

Thanks for your hard work so far!

[Explanation and examples elided]

To what extent, if any, are you attempting to follow the SQL:2011
standard?

http://cs.ulb.ac.be/public/_media/teaching/infoh415/tempfeaturessql2011.pdf

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david(dot)fetter(at)gmail(dot)com

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3Anton Dignös
dignoes@inf.unibz.it
In reply to: David Fetter (#2)
Re: [PROPOSAL] Temporal query processing with range types

On Sat, Jul 23, 2016 at 12:01 AM, David Fetter <david@fetter.org> wrote:

On Fri, Jul 22, 2016 at 01:15:17PM +0200, Anton Dignös wrote:

Hi hackers,

we are a group of researches that work on temporal databases. Our
main focus is the processing of data with time intervals, such as
the range types in PostgreSQL.

Thanks for your hard work so far!

[Explanation and examples elided]

To what extent, if any, are you attempting to follow the SQL:2011
standard?

http://cs.ulb.ac.be/public/_media/teaching/infoh415/tempfeaturessql2011.pdf

The querying in the SQL:2011 standard is based on simple SQL range restrictions
and period predicates (OVERLAP, PRECEDES, FOR SYSTEM_TIME AS OF, etc) that
functionality-wise in PostgreSQL are already covered by the operators and
functions on range types.

Operations such as aggregation, outer joins, set-operations on ranges
(mentioned in
Section 2.5 "Future directions" in the above paper) are not yet part of the
standard. These are the operations that require the adjustment (or splitting) of
ranges.

Best,

Anton

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#4Robert Haas
robertmhaas@gmail.com
In reply to: Anton Dignös (#1)
Re: [PROPOSAL] Temporal query processing with range types

On Fri, Jul 22, 2016 at 7:15 AM, Anton Dignös <dignoes@inf.unibz.it> wrote:

We would like to contribute to PostgreSQL a solution that supports the query
processing of "at each time point". The basic idea is to offer two new
operators, NORMALIZE and ALIGN, whose purpose is to adjust (or split) the
ranges of tuples so that subsequent queries can use the usual grouping and
equality conditions to get the intended results.

I think that it is great that you want to contribute your work to
PostgreSQL. I don't know whether there will be a consensus that this
is generally useful functionality that we should accept, but I commend
the effort anyhow. Assuming there is, getting this into a state that
we consider committable will probably take quite a bit of additional
work on your part; no one will do it for you. If you're still
interested in proceeding given those caveats, please add your patch
here so that it gets reviewed:

https://commitfest.postgresql.org/action/commitfest_view/open

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#5Peter Moser
pitiz29a@gmail.com
In reply to: Robert Haas (#4)
Re: [PROPOSAL] Temporal query processing with range types

On 27.07.2016 at 16:09 Robert Haas wrote:

On Fri, Jul 22, 2016 at 7:15 AM, Anton Dignös <dignoes@inf.unibz.it> wrote:

We would like to contribute to PostgreSQL a solution that supports the query
processing of "at each time point". The basic idea is to offer two new
operators, NORMALIZE and ALIGN, whose purpose is to adjust (or split) the
ranges of tuples so that subsequent queries can use the usual grouping and
equality conditions to get the intended results.

I think that it is great that you want to contribute your work to
PostgreSQL. I don't know whether there will be a consensus that this
is generally useful functionality that we should accept, but I commend
the effort anyhow. Assuming there is, getting this into a state that
we consider committable will probably take quite a bit of additional
work on your part; no one will do it for you.

Hi hackers,

thank you for your feedback.

We are aware that contributing to PostgreSQL is a long way with a lot
of work. We are committed to go all the way and do the work as
discussed in the community.

We had some internal discussions about the project, looking also at
some other patches to better understand whether the patch is
work-in-progress or ready for commitfest.

If you're still
interested in proceeding given those caveats, please add your patch
here so that it gets reviewed:

https://commitfest.postgresql.org/action/commitfest_view/open

We decided to follow your recommendation and add the patch to the
commitfest.

Looking forward for your feedback,
Anton, Johann, Michael, Peter

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#6Haribabu Kommi
kommi.haribabu@gmail.com
In reply to: Peter Moser (#5)
Re: [PROPOSAL] Temporal query processing with range types

On Tue, Oct 25, 2016 at 8:44 PM, Peter Moser <pitiz29a@gmail.com> wrote:

We decided to follow your recommendation and add the patch to the
commitfest.

Path is not applying properly to HEAD.
Moved to next CF with "waiting on author" status.

Regards,
Hari Babu
Fujitsu Australia

#7Peter Moser
pitiz29a@gmail.com
In reply to: Haribabu Kommi (#6)
1 attachment(s)
Re: [PROPOSAL] Temporal query processing with range types

Am 05.12.2016 um 06:11 schrieb Haribabu Kommi:

On Tue, Oct 25, 2016 at 8:44 PM, Peter Moser <pitiz29a@gmail.com
<mailto:pitiz29a@gmail.com>> wrote:

We decided to follow your recommendation and add the patch to the
commitfest.

Path is not applying properly to HEAD.
Moved to next CF with "waiting on author" status.

We updated our patch. We tested it with the latest
commit dfe530a09226a9de80f2b4c3d5f667bf51481c49.

Regards,
Hari Babu
Fujitsu Australia

Best regards,
Anton, Johann, Michael, Peter

Attachments:

tpg_primitives_out_v2.patchtext/x-patch; name=tpg_primitives_out_v2.patchDownload
diff --git src/backend/commands/explain.c src/backend/commands/explain.c
index 0a669d9..09406d4 100644
--- src/backend/commands/explain.c
+++ src/backend/commands/explain.c
@@ -875,6 +875,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 src/backend/executor/Makefile src/backend/executor/Makefile
index 51edd4c..42801d3 100644
--- src/backend/executor/Makefile
+++ src/backend/executor/Makefile
@@ -25,6 +25,8 @@ OBJS = execAmi.o execCurrent.o execGrouping.o execIndexing.o execJunk.o \
        nodeSamplescan.o nodeSeqscan.o nodeSetOp.o nodeSort.o nodeUnique.o \
        nodeValuesscan.o nodeCtescan.o nodeWorktablescan.o \
        nodeGroup.o nodeSubplan.o nodeSubqueryscan.o nodeTidscan.o \
-       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o
+       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o \
+       nodeTemporalAdjustment.o
+
 
 include $(top_srcdir)/src/backend/common.mk
diff --git src/backend/executor/execProcnode.c src/backend/executor/execProcnode.c
index 554244f..610d753 100644
--- src/backend/executor/execProcnode.c
+++ src/backend/executor/execProcnode.c
@@ -114,6 +114,7 @@
 #include "executor/nodeValuesscan.h"
 #include "executor/nodeWindowAgg.h"
 #include "executor/nodeWorktablescan.h"
+#include "executor/nodeTemporalAdjustment.h"
 #include "nodes/nodeFuncs.h"
 #include "miscadmin.h"
 
@@ -334,6 +335,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 */
@@ -531,6 +537,10 @@ ExecProcNode(PlanState *node)
 			result = ExecLimit((LimitState *) node);
 			break;
 
+		case T_TemporalAdjustmentState:
+			result = ExecTemporalAdjustment((TemporalAdjustmentState *) node);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
 			result = NULL;
@@ -779,6 +789,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;
@@ -812,3 +826,4 @@ ExecShutdownNode(PlanState *node)
 
 	return planstate_tree_walker(node, ExecShutdownNode, NULL);
 }
+
diff --git src/backend/executor/nodeTemporalAdjustment.c src/backend/executor/nodeTemporalAdjustment.c
new file mode 100644
index 0000000..e45ec03
--- /dev/null
+++ src/backend/executor/nodeTemporalAdjustment.c
@@ -0,0 +1,528 @@
+#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 */
+
+/*
+ * #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);
+}
+
+/*
+ * 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->attNumTs - 1] = newTs;
+	node->newValues[node->temporalCl->attNumTe - 1] = newTe;
+	if(node->temporalCl->attNumTr != -1)
+		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);
+
+/*
+ * 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.ts, r.te, s.ts, s.te)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1, P2)
+ * 	  where r_1,...,r_n are all attributes from relation r. Two of these
+ * 	  attributes are temporal boundaries, namely TS and TE. The interval
+ * 	  [TS,TE) 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.ts, r.te, s.ts, s.te)) x
+ * 	  or
+ * 	  ... FROM (r NORMALIZE s ON theta WITH (r.ts, r.te, s.ts, s.te)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1)
+ * 	  where r_1,...,r_n are all attributes from relation r. Two of these
+ * 	  attributes are temporal boundaries, namely TS and TE. The interval
+ * 	  [TS,TE) represents the VALID TIME of each tuple. RN is the
+ * 	  temporal group ID or row number, and P1 is union of both
+ * 	  timepoints TS and TE 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;
+	Datum 				 currP2;
+	Datum				 currRN;
+	Datum				 prevRN;
+	Datum				 prevTe;
+
+	if(node->firstCall)
+	{
+		curr = ExecProcNode(outerPlan);
+		if(TupIsNull(curr))
+			return NULL;
+
+		prev = ExecCopySlot(prev, curr);
+		node->sameleft = true;
+		node->firstCall = false;
+		node->outrn = 0;
+		node->datumFormat = curr->tts_tupleDescriptor->attrs[tc->attNumTs - 1];
+		setSweepline(slotGetAttrNotNull(curr, tc->attNumTs));
+	}
+
+	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;
+				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(heapGetAttrNotNull(out, tc->attNumTs),
+									currP1,
+									node)
+						|| !isEqual(heapGetAttrNotNull(out, tc->attNumTe),
+									currP2,
+									node)
+						|| node->outrn != DatumGetInt64(currRN))
+					{
+						temporalAdjustmentStoreTuple(node, curr, out,
+													 currP1, currP2);
+
+						/* sweepline = max(sweepline, curr.P2) */
+						if (isLessThan(node->sweepline, currP2, node))
+							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 = heapGetAttrNotNull(prev, tc->attNumTe);
+
+			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);
+				setSweepline(slotGetAttrNotNull(curr, tc->attNumTs));
+			}
+			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));
+
+	/* 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;
+
+	/*
+	 * Miscellaneous initialization
+	 *
+	 * Unique 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.
+	 */
+	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 align nodes do no projections, so initialize projection info for
+	 * this node appropriately
+	 */
+	ExecAssignResultTypeFromTL(&state->ss.ps);
+	state->ss.ps.ps_ProjInfo = NULL;
+	state->ss.ps.ps_TupFromTlist = false;
+
+	state->alignment = node->temporalCl->temporalType == TEMPORAL_TYPE_ALIGNER;
+	state->temporalCl = copyObject(node->temporalCl);
+	state->firstCall = true;
+
+	/*
+	 * Init masks
+	 */
+	state->nullMask = palloc0(sizeof(bool) * node->numCols);
+	state->tsteMask = palloc0(sizeof(bool) * node->numCols);
+
+	state->tsteMask[state->temporalCl->attNumTs - 1] = true;
+	state->tsteMask[state->temporalCl->attNumTe - 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);
+
+	/*
+	 * Range types in boundaries need special treatment:
+	 * - there is an extra column in each tuple that must be changed
+	 * - and a range constructor method that must be called
+	 */
+	if(node->temporalCl->attNumTr != -1)
+	{
+		state->tsteMask[state->temporalCl->attNumTr - 1] = true;
+		fmgr_info(fmgr_internal_function("range_constructor2"), rcFunctionInfo);
+		rcFunctionInfo->fn_expr = (fmNodePtr) node->rangeVar;
+		InitFunctionCallInfoData(state->rcFuncCallInfo, rcFunctionInfo, 2,
+								 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));
+}
+
+
+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 src/backend/nodes/copyfuncs.c src/backend/nodes/copyfuncs.c
index dd66adb..e957475 100644
--- src/backend/nodes/copyfuncs.c
+++ src/backend/nodes/copyfuncs.c
@@ -1949,6 +1949,9 @@ _copyJoinExpr(const JoinExpr *from)
 	COPY_NODE_FIELD(quals);
 	COPY_NODE_FIELD(alias);
 	COPY_SCALAR_FIELD(rtindex);
+	COPY_NODE_FIELD(temporalBounds);
+	COPY_SCALAR_FIELD(inTmpPrimTempType);
+	COPY_SCALAR_FIELD(inTmpPrimHasRangeT);
 
 	return newnode;
 }
@@ -2309,6 +2312,45 @@ _copyOnConflictClause(const OnConflictClause *from)
 	return newnode;
 }
 
+static TemporalClause *
+_copyTemporalClause(const TemporalClause *from)
+{
+	TemporalClause *newnode = makeNode(TemporalClause);
+
+	COPY_SCALAR_FIELD(temporalType);
+	COPY_SCALAR_FIELD(attNumTs);
+	COPY_SCALAR_FIELD(attNumTe);
+	COPY_SCALAR_FIELD(attNumTr);
+	COPY_SCALAR_FIELD(attNumP1);
+	COPY_SCALAR_FIELD(attNumP2);
+	COPY_SCALAR_FIELD(attNumRN);
+	COPY_STRING_FIELD(colnameTs);
+	COPY_STRING_FIELD(colnameTe);
+	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)
 {
@@ -2768,6 +2810,7 @@ _copyQuery(const Query *from)
 	COPY_NODE_FIELD(setOperations);
 	COPY_NODE_FIELD(constraintDeps);
 	COPY_NODE_FIELD(withCheckOptions);
+	COPY_NODE_FIELD(temporalClause);
 
 	return newnode;
 }
@@ -2835,6 +2878,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);
@@ -5102,6 +5146,12 @@ copyObject(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 src/backend/nodes/equalfuncs.c src/backend/nodes/equalfuncs.c
index cad3aeb..b5e4fb2 100644
--- src/backend/nodes/equalfuncs.c
+++ src/backend/nodes/equalfuncs.c
@@ -755,6 +755,9 @@ _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);
+	COMPARE_SCALAR_FIELD(inTmpPrimHasRangeT);
 
 	return true;
 }
diff --git src/backend/nodes/makefuncs.c src/backend/nodes/makefuncs.c
index 20e2dbd..ff6feab 100644
--- src/backend/nodes/makefuncs.c
+++ 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 -
@@ -610,3 +693,4 @@ makeGroupingSet(GroupingSetKind kind, List *content, int location)
 	n->location = location;
 	return n;
 }
+
diff --git src/backend/nodes/outfuncs.c src/backend/nodes/outfuncs.c
index 748b687..2c03e36 100644
--- src/backend/nodes/outfuncs.c
+++ src/backend/nodes/outfuncs.c
@@ -1548,6 +1548,9 @@ _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);
+	WRITE_BOOL_FIELD(inTmpPrimHasRangeT);
 }
 
 static void
@@ -1816,6 +1819,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");
@@ -2496,6 +2511,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);
@@ -2572,6 +2588,38 @@ _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(attNumTs);
+	WRITE_INT_FIELD(attNumTe);
+	WRITE_INT_FIELD(attNumTr);
+	WRITE_INT_FIELD(attNumP1);
+	WRITE_INT_FIELD(attNumP2);
+	WRITE_INT_FIELD(attNumRN);
+	WRITE_STRING_FIELD(colnameTs);
+	WRITE_STRING_FIELD(colnameTe);
+	WRITE_STRING_FIELD(colnameTr);
+}
+
+static void
 _outColumnDef(StringInfo str, const ColumnDef *node)
 {
 	WRITE_NODE_TYPE("COLUMNDEF");
@@ -2703,6 +2751,7 @@ _outQuery(StringInfo str, const Query *node)
 	WRITE_NODE_FIELD(rowMarks);
 	WRITE_NODE_FIELD(setOperations);
 	WRITE_NODE_FIELD(constraintDeps);
+	WRITE_NODE_FIELD(temporalClause);
 }
 
 static void
@@ -3626,6 +3675,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;
@@ -3723,6 +3775,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);
@@ -3867,7 +3925,6 @@ outNode(StringInfo str, const void *obj)
 				break;
 
 			default:
-
 				/*
 				 * This should be an ERROR, but it's too useful to be able to
 				 * dump structures that outNode only understands part of.
diff --git src/backend/nodes/print.c src/backend/nodes/print.c
index a1f2941..5d73c41 100644
--- src/backend/nodes/print.c
+++ src/backend/nodes/print.c
@@ -25,7 +25,7 @@
 #include "optimizer/clauses.h"
 #include "parser/parsetree.h"
 #include "utils/lsyscache.h"
-
+#include "parser/parse_node.h"
 
 /*
  * print
@@ -492,3 +492,18 @@ print_slot(TupleTableSlot *slot)
 
 	debugtup(slot, NULL);
 }
+
+/*
+ * print_namespace
+ * 		print out all name space items' RTEs.
+ */
+void
+print_namespace(const List *namespace)
+{
+	ListCell   *lc;
+	foreach(lc, namespace)
+	{
+		ParseNamespaceItem *nsitem = (ParseNamespaceItem *) lfirst(lc);
+		pprint(nsitem->p_rte);
+	}
+}
diff --git src/backend/nodes/readfuncs.c src/backend/nodes/readfuncs.c
index 917e6c8..b9d2ce8 100644
--- src/backend/nodes/readfuncs.c
+++ 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);
 
 	READ_DONE();
 }
@@ -423,6 +424,28 @@ _readSetOperationStmt(void)
 	READ_DONE();
 }
 
+/*
+ * _readTemporalClause
+ */
+static TemporalClause *
+_readTemporalClause(void)
+{
+	READ_LOCALS(TemporalClause);
+
+	READ_ENUM_FIELD(temporalType, TemporalType);
+	READ_INT_FIELD(attNumTs);
+	READ_INT_FIELD(attNumTe);
+	READ_INT_FIELD(attNumTr);
+	READ_INT_FIELD(attNumP1);
+	READ_INT_FIELD(attNumP2);
+	READ_INT_FIELD(attNumRN);
+	READ_STRING_FIELD(colnameTs);
+	READ_STRING_FIELD(colnameTe);
+	READ_STRING_FIELD(colnameTr);
+
+	READ_DONE();
+}
+
 
 /*
  *	Stuff from primnodes.h.
@@ -457,6 +480,17 @@ _readRangeVar(void)
 	READ_DONE();
 }
 
+static ColumnRef *
+_readColumnRef(void)
+{
+	READ_LOCALS(ColumnRef);
+
+	READ_NODE_FIELD(fields);
+	READ_LOCATION_FIELD(location);
+
+	READ_DONE();
+}
+
 static IntoClause *
 _readIntoClause(void)
 {
@@ -1238,6 +1272,9 @@ _readJoinExpr(void)
 	READ_NODE_FIELD(quals);
 	READ_NODE_FIELD(alias);
 	READ_INT_FIELD(rtindex);
+	READ_NODE_FIELD(temporalBounds);
+	READ_ENUM_FIELD(inTmpPrimTempType, TemporalType);
+	READ_BOOL_FIELD(inTmpPrimHasRangeT);
 
 	READ_DONE();
 }
@@ -2407,6 +2444,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 src/backend/optimizer/path/allpaths.c src/backend/optimizer/path/allpaths.c
index 9753a26..077c502 100644
--- src/backend/optimizer/path/allpaths.c
+++ 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 */
@@ -126,6 +127,7 @@ static void subquery_push_qual(Query *subquery,
 static void recurse_push_qual(Node *setOp, Query *topquery,
 				  RangeTblEntry *rte, Index rti, Node *qual);
 static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
+static bool allWindowFuncsHaveRowId(List *targetList);
 
 
 /*
@@ -2307,7 +2309,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;
 
 	/*
@@ -2417,6 +2420,7 @@ static void
 check_output_expressions(Query *subquery, pushdown_safety_info *safetyInfo)
 {
 	ListCell   *lc;
+	bool wfsafe = allWindowFuncsHaveRowId(subquery->targetList);
 
 	foreach(lc, subquery->targetList)
 	{
@@ -2455,12 +2459,35 @@ 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 resnoTs =
+					((TemporalClause *)subquery->temporalClause)->attNumTs;
+			AttrNumber resnoTe =
+					((TemporalClause *)subquery->temporalClause)->attNumTe;
+			AttrNumber resnoRangeT =
+					((TemporalClause *)subquery->temporalClause)->attNumTr;
+
+			if (tle->resno == resnoTs
+					|| tle->resno == resnoTe
+					|| tle->resno == resnoRangeT)
+			{
+				safetyInfo->unsafeColumns[tle->resno] = true;
+				continue;
+			}
+		}
 	}
 }
 
@@ -2530,6 +2557,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
@@ -2813,6 +2866,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 src/backend/optimizer/plan/createplan.c src/backend/optimizer/plan/createplan.c
index ad49674..a3f590f 100644
--- src/backend/optimizer/plan/createplan.c
+++ src/backend/optimizer/plan/createplan.c
@@ -113,6 +113,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,
@@ -270,6 +273,9 @@ static ModifyTable *make_modifytable(PlannerInfo *root,
 				 List *resultRelations, List *subplans,
 				 List *withCheckOptionLists, List *returningLists,
 				 List *rowMarks, OnConflictExpr *onconflict, int epqParam);
+static TemporalAdjustment *make_temporalAdjustment(Plan *lefttree,
+						TemporalClause *temporalClause,
+						List *sortClause);
 
 
 /*
@@ -463,6 +469,11 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
 											  (LimitPath *) best_path,
 											  flags);
 			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);
@@ -2246,6 +2257,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;
+}
+
 
 /*****************************************************************************
  *
@@ -4826,6 +4864,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 = 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 src/backend/optimizer/plan/planner.c src/backend/optimizer/plan/planner.c
index 41dde50..0f76232 100644
--- src/backend/optimizer/plan/planner.c
+++ src/backend/optimizer/plan/planner.c
@@ -1987,6 +1987,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
@@ -4358,7 +4372,6 @@ create_ordered_paths(PlannerInfo *root,
 	return ordered_rel;
 }
 
-
 /*
  * make_group_input_target
  *	  Generate appropriate PathTarget for initial input to grouping nodes.
diff --git src/backend/optimizer/plan/setrefs.c src/backend/optimizer/plan/setrefs.c
index d91bc3b..c5692f2 100644
--- src/backend/optimizer/plan/setrefs.c
+++ src/backend/optimizer/plan/setrefs.c
@@ -613,6 +613,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 src/backend/optimizer/plan/subselect.c src/backend/optimizer/plan/subselect.c
index 3171743..a1c9733 100644
--- src/backend/optimizer/plan/subselect.c
+++ src/backend/optimizer/plan/subselect.c
@@ -2687,6 +2687,7 @@ finalize_plan(PlannerInfo *root, Plan *plan, Bitmapset *valid_params,
 		case T_Gather:
 		case T_SetOp:
 		case T_Group:
+		case T_TemporalAdjustment:
 			break;
 
 		default:
diff --git src/backend/optimizer/util/pathnode.c src/backend/optimizer/util/pathnode.c
index 6d3ccfd..e6f92a0 100644
--- src/backend/optimizer/util/pathnode.c
+++ src/backend/optimizer/util/pathnode.c
@@ -2362,6 +2362,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 src/backend/parser/Makefile src/backend/parser/Makefile
index fdd8485..35c20fa 100644
--- src/backend/parser/Makefile
+++ src/backend/parser/Makefile
@@ -15,7 +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_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_relation.o parse_target.o parse_type.o parse_utilcmd.o scansup.o \
+      parse_temporal.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git src/backend/parser/analyze.c src/backend/parser/analyze.c
index 1a54178..4412332 100644
--- src/backend/parser/analyze.c
+++ 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"
@@ -1169,6 +1170,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");
@@ -1243,6 +1247,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 src/backend/parser/gram.y src/backend/parser/gram.y
index 414348b..4c42566 100644
--- src/backend/parser/gram.y
+++ src/backend/parser/gram.y
@@ -407,6 +407,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
@@ -458,11 +459,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
@@ -545,6 +549,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				opt_frame_clause frame_extent frame_bound
 %type <str>		opt_existing_window_name
 %type <boolean> opt_if_not_exists
+%type <list>    temporal_bounds_list
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -569,7 +574,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 ATTRIBUTE AUTHORIZATION
 
 	BACKWARD BEFORE BEGIN_P BETWEEN BIGINT BINARY BIT
@@ -614,7 +619,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
 
@@ -10505,6 +10510,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.
@@ -10761,6 +10779,94 @@ 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 period boundaries with 4 comparable values
+					 * or two range typed values,
+					 * i.e. [lts, lte) is the left argument period, and
+					 *		[rts, rte) is the right argument period,
+					 * or two compatible range types with bounds like '[)'
+					 */
+					if(list_length($6) == 4 || list_length($6) == 2)
+						n->temporalBounds = $6;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have 2 range typed values, or four " \
+										"single 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 period boundaries with 4 comparable values
+					 * or two range typed values,
+					 * i.e. [lts, lte) is the left argument period, and
+					 *      [rts, rte) is the right argument period,
+					 * or two compatible range types with bounds like '[)'
+					 */
+					if(list_length($5) == 4 || list_length($5) == 2)
+						n->temporalBounds = $5;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have 2 range typed values, or four " \
+										"single values."),
+								 parser_errposition(@5)));
+
+					$$ = n;
+				}
 		;
 
 
@@ -14069,7 +14175,8 @@ type_func_name_keyword:
  * forced to.
  */
 reserved_keyword:
-			  ALL
+			  ALIGN
+			| ALL
 			| ANALYSE
 			| ANALYZE
 			| AND
@@ -14117,6 +14224,7 @@ reserved_keyword:
 			| LIMIT
 			| LOCALTIME
 			| LOCALTIMESTAMP
+			| NORMALIZE
 			| NOT
 			| NULL_P
 			| OFFSET
diff --git src/backend/parser/parse_clause.c src/backend/parser/parse_clause.c
index 751de4b..47d3391 100644
--- src/backend/parser/parse_clause.c
+++ src/backend/parser/parse_clause.c
@@ -39,6 +39,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"
@@ -967,6 +968,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.
 		 */
@@ -1029,6 +1067,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 src/backend/parser/parse_temporal.c src/backend/parser/parse_temporal.c
new file mode 100644
index 0000000..3a3f86f
--- /dev/null
+++ src/backend/parser/parse_temporal.c
@@ -0,0 +1,1620 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 can have four elements in a boundary
+ * list (i.e., WITH-clause of a temporal primitive) when we have two argument
+ * relations with scalar boundaries, or two entries if we have 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_LARGTST = 0,
+	TPB_LARGTE,
+	TPB_RARGTST,
+	TPB_RARGTE
+} 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,
+						  bool *hasRangeTypes,
+						  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,
+						   bool hasRangeTypes,
+						   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. These are either
+ * 		two scalar values for TS and TE, or a single range type value T holding
+ * 		both bounds.
+ */
+static List *
+temporalBoundGetLeftBounds(List *bounds)
+{
+	switch(list_length(bounds))
+	{
+		case 2: return list_make1(linitial(bounds));
+		case 4: return list_make2(linitial(bounds), lsecond(bounds));
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT),
+			 errmsg("Invalid temporal bound list length."),
+			 errhint("Specify four scalar columns for the " \
+					 "temporal boundaries, or two range-typed "\
+					 "columns.")));
+
+	/* Keep compiler quiet */
+	return NIL;
+}
+
+/*
+ * temporalBoundGetRightBounds -
+ * 		Return the right boundaries of a temporal bounds list. These are either
+ * 		two scalar values for TS and TE, or a single range type value T holding
+ * 		both bounds.
+ */
+static List *
+temporalBoundGetRightBounds(List *bounds)
+{
+	switch(list_length(bounds))
+	{
+		case 2: return list_make1(lsecond(bounds));
+		case 4: return list_make2(lthird(bounds), lfourth(bounds));
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT),
+			 errmsg("Invalid temporal bound list length."),
+			 errhint("Specify four scalar columns for the " \
+					 "temporal boundaries, or two range-typed "\
+					 "columns.")));
+
+	/* Keep compiler quiet */
+	return NIL;
+}
+
+/*
+ * 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)
+	{
+		/*
+		 * Four boundary entries means that we have 2x two scalar boundaries.
+		 * Which means the first two entries are start and end of the first
+		 * bound, and the 3th and 4th entry are start and end of the second
+		 * bound.
+		 */
+		case 4:
+			return list_nth(bounds, id);
+
+		/*
+		 * 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_LARGTST)
+				return linitial(bounds);
+			if(id == TPB_RARGTST || id == TPB_LARGTE)
+				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_LARGTST)
+				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 four scalar columns for the " \
+						 "temporal boundaries, or two range-typed "\
+						 "columns.")));
+
+	return NULL;
+}
+
+/*
+ * transformTemporalClause -
+ * 		If we have a temporal primitive query, we must find all attribute
+ * 		numbers for p1, p2, rn, ts, te, 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, all attribute numbers for temporal boundaries are unknown */
+	tc->attNumTr = -1;
+	tc->attNumTe = -1;
+	tc->attNumTs = -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;
+	}
+
+	/*
+	 * If we have range types the subquery splits it into separate
+	 * columns, called ts and te which are in between the p1- and
+	 * rn-column.
+	 */
+	if(tc->colnameTr)
+	{
+		tc->attNumTe = pos--;
+		tc->attNumTs = pos--;
+	}
+
+	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 if(strcmp(key, "ts") == 0)
+		{
+			tc->colnameTs = pstrdup(value);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumTs);
+			foundTsTe = true;
+		}
+		else if(strcmp(key, "te") == 0)
+		{
+			tc->colnameTe = pstrdup(value);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumTe);
+			foundTsTe = true;
+		}
+		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;
+
+		/* Temporal boundary is a range type */
+		if (tc->colnameTr)
+		{
+			if (strcmp(tle->resname, tc->colnameTr) == 0)
+				tc->attNumTr = tle->resno;
+		}
+		else /* Two scalar columns for boundaries */
+		{
+			if (strcmp(tle->resname, tc->colnameTs) == 0)
+				tc->attNumTs = tle->resno;
+			else if (strcmp(tle->resname, tc->colnameTe) == 0)
+				tc->attNumTe = tle->resno;
+		}
+	}
+
+	/* We need column attribute numbers for all temporal boundaries */
+	if(tc->attNumTs == -1
+			|| tc->attNumTe == -1
+			|| (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;
+
+	/* We use range types, remove splitted columns, i.e. upper/lower bounds */
+	if(tc->colnameTr) {
+		get_tle_by_resno(qry->targetList, tc->attNumTs)->resjunk = true;
+		get_tle_by_resno(qry->targetList, tc->attNumTe)->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, bool *hasRangeTypes, 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;
+
+	/*
+	 * We can have 2 or 4 column references, i.e. if we have 4, the first two
+	 * form the left argument period [largTs, largTe), and the last two the
+	 * right argument period respectively. Otherwise, we have two range typed
+	 * values of the form '[)' where the first argument contains the boundaries
+	 * of the left-hand-side, and the second argument contains the boundaries
+	 * of the RHS respectively. The parser checked already if there was another
+	 * number of arguments (not equal to 2 or 4) given.
+	 */
+	*hasRangeTypes = list_length(j->temporalBounds) == 2;
+
+	/*
+	 * 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);
+	if(*hasRangeTypes)
+	{
+		/*
+		 * Hardcoded column names for ts and te. We handle ambiguous column
+		 * names during the transformation of temporal primitive clauses.
+		 */
+		ssResult->temporalClause->colnameTs = "ts";
+		ssResult->temporalClause->colnameTe = "te";
+		ssResult->temporalClause->colnameTr =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTST);
+	}
+	else
+	{
+		ssResult->temporalClause->colnameTs =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTST);
+		ssResult->temporalClause->colnameTe =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTE);
+		ssResult->temporalClause->colnameTr = NULL;
+	}
+
+	/*
+	 * 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;
+	joinExpr->inTmpPrimHasRangeT = *hasRangeTypes;
+
+	return ssResult;
+}
+
+/*
+ * transformTemporalAligner -
+ * 		transform a TEMPORAL ALIGN clause into standard SQL
+ *
+ * INPUT:
+ * 		(r ALIGN s ON q WITH (r.ts, r.te, s.ts, s.te)) c
+ * 		where q can be any join qualifier, and r.ts, r.te, s.ts, and s.te
+ * 		can be any column name.
+ *
+ * OUTPUT:
+ * 		(
+ * 		SELECT r.*, GREATEST(r.ts, s.ts) P1, LEAST(r.te, s.te) P2
+ *      FROM
+ *      (
+ *      	SELECT *, row_id() OVER () rn FROM r
+ *      ) r
+ *      LEFT OUTER JOIN
+ *      s
+ *      ON q AND r.ts < s.te AND r.te > s.ts
+ *      ORDER BY rn, P1, P2
+ *      ) c
+ */
+Node *
+transformTemporalAligner(ParseState *pstate, JoinExpr *j)
+{
+	const int 	 		 UNKNOWN_LOCATION = -1;
+
+	bool				 hasRangeTypes;
+	SelectStmt			*ssResult;
+	RangeSubselect 		*rssResult;
+	ResTarget			*rtGreatest;
+	ResTarget			*rtLeast;
+	ResTarget			*rtLowerLarg;
+	ResTarget			*rtUpperLarg;
+	ColumnRef 			*crLargTs;
+	ColumnRef 			*crRargTs;
+	ColumnRef 			*crLargTe;
+	ColumnRef 			*crRargTe;
+	MinMaxExpr			*mmeGreatest;
+	MinMaxExpr			*mmeLeast;
+	FuncCall			*fcLowerLarg;
+	FuncCall			*fcLowerRarg;
+	FuncCall			*fcUpperLarg;
+	FuncCall			*fcUpperRarg;
+	List				*mmeGreatestArgs;
+	List				*mmeLeastArgs;
+	List				*boundariesExpr;
+	JoinExpr			*joinExpr;
+	A_Expr				*lowerBoundExpr;
+	A_Expr				*upperBoundExpr;
+	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, &hasRangeTypes,
+										 &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.
+	 */
+	if (hasRangeTypes)
+	{
+		crLargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crRargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   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);
+
+		rtLowerLarg = makeResTarget((Node *) fcLowerLarg,
+									ssResult->temporalClause->colnameTs);
+		rtUpperLarg = makeResTarget((Node *) fcUpperLarg,
+									ssResult->temporalClause->colnameTe);
+
+		ssResult->targetList = list_concat(ssResult->targetList,
+										   list_make2(rtLowerLarg, rtUpperLarg));
+	}
+	else
+	{
+		crLargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crLargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTE,
+										   largAlias->aliasname);
+		crRargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+		crRargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTE,
+										   rargAlias->aliasname);
+
+		/* Create argument list for function call to "greatest" and "least" */
+		mmeGreatestArgs = list_make2(crLargTs, crRargTs);
+		mmeLeastArgs = list_make2(crLargTe, crRargTe);
+
+		/*
+		 * Build Boolean expressions, i.e. "r.ts < s.te AND r.te > s.ts"
+		 * and concatenate it with q (=theta)
+		 */
+		lowerBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  "<",
+										  copyObject(crLargTs),
+										  copyObject(crRargTe),
+										  UNKNOWN_LOCATION);
+		upperBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  ">",
+										  copyObject(crLargTe),
+										  copyObject(crRargTs),
+										  UNKNOWN_LOCATION);
+
+		boundariesExpr = list_make2(lowerBoundExpr, upperBoundExpr);
+	}
+
+	/* 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.ts, r.te, s.ts, s.te)) c
+ *
+ * 		-- or --
+ *
+ * 		(r NORMALIZE s USING(atts) WITH (r.ts, r.te, s.ts, s.te)) c
+ * 		where q can be any join qualifier and r.ts, r.te, s.ts, and s.te
+ * 		can be any column name.
+ *
+ * OUTPUT:
+ * 		(
+ * 			SELECT r.*,
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT s.*, ts P1 FROM s
+ *      		UNION ALL
+ *      		SELECT s.*, te P1 FROM s
+ *      	) s
+ *      	ON q AND P1 >= r.ts AND P1 < r.te
+ *      	ORDER BY rn, P1
+ *      ) c
+ *
+ *      -- or --
+ *
+ * 		(
+ * 			SELECT r.*,
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT atts, ts P1 FROM s
+ *      		UNION
+ *      		SELECT atts, te P1 FROM s
+ *      	) s
+ *      	ON r.atts = s.atts AND P1 >= r.ts AND P1 < r.te
+ *      	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;
+	ResTarget			*rtLowerLarg;
+	ResTarget			*rtUpperLarg;
+	ColumnRef 			*crRargStar;
+	ColumnRef 			*crLargTsT = NULL;
+	ColumnRef 			*crRargTsT = NULL;
+	ColumnRef 			*crLargTe = NULL;
+	ColumnRef 			*crRargTe = NULL;
+	ColumnRef 			*crP1;
+	JoinExpr			*joinExpr;
+	A_Expr				*lowerBoundExpr;
+	A_Expr				*upperBoundExpr;
+	A_Expr				*containsExpr;
+	Node				*boolExpr;
+	Alias				*largAlias;
+	Alias				*rargAlias;
+	char 				*colnameRN;
+	char 				*colnameP1;
+	bool				 hasRangeTypes;
+	FuncCall			*fcLowerLarg = NULL;
+	FuncCall			*fcUpperLarg = NULL;
+	FuncCall			*fcLowerRarg = NULL;
+	FuncCall			*fcUpperRarg = NULL;
+	List				*boundariesExpr;
+
+	/* Create a select statement skeleton to be filled here */
+	ssResult = makeTemporalQuerySkeleton(j, &colnameRN, &colnameP1,
+										 NULL, &hasRangeTypes,
+										 &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. If we need only two range types
+	 * only Ts columnrefs are used.
+	 */
+	if (hasRangeTypes)
+	{
+		crLargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crRargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+
+		/* Create argument list for function call to "greatest" and "least" */
+		fcLowerLarg = makeFuncCall(SystemFuncName("lower"),
+								   list_make1(crLargTsT),
+								   UNKNOWN_LOCATION);
+		fcLowerRarg = makeFuncCall(SystemFuncName("lower"),
+								   list_make1(crRargTsT),
+								   UNKNOWN_LOCATION);
+		fcUpperLarg = makeFuncCall(SystemFuncName("upper"),
+								   list_make1(crLargTsT),
+								   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);
+
+		rtLowerLarg = makeResTarget((Node *) fcLowerLarg,
+									ssResult->temporalClause->colnameTs);
+		rtUpperLarg = makeResTarget((Node *) fcUpperLarg,
+									ssResult->temporalClause->colnameTe);
+
+		ssResult->targetList = list_concat(ssResult->targetList,
+										   list_make2(rtLowerLarg, rtUpperLarg));
+		/*
+		 * 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);
+	}
+	else
+	{
+		/*
+		 * Build column references, for use later.
+		 */
+		crLargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crLargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTE,
+										   largAlias->aliasname);
+		crRargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+		crRargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTE,
+										   rargAlias->aliasname);
+
+		/* Build resource target "ts P1" and "te P1" */
+		rtTsP1 = makeResTarget((Node *) crRargTsT, colnameP1);
+		rtTeP1 = makeResTarget((Node *) crRargTe, colnameP1);
+		/*
+		 * Build "contains" expressions, i.e. "P1 >= ts AND P1 < te"
+		 * and concatenate it with q (=theta)
+		 */
+		lowerBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  ">=",
+										  copyObject(crP1),
+										  copyObject(crLargTsT),
+										  UNKNOWN_LOCATION);
+		upperBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  "<",
+										  copyObject(crP1),
+										  copyObject(crLargTe),
+										  UNKNOWN_LOCATION);
+
+		boundariesExpr = list_make2(lowerBoundExpr, upperBoundExpr);
+	}
+
+	/*
+	 * Build "SELECT s.*, ts P1 FROM s" and "SELECT s.*, te P1 FROM s", iff we
+	 * have a ON-clause.
+	 * If we have an USING-clause with a name-list 'atts' build "SELECT atts,
+	 * ts P1 FROM s" and "SELECT atts, te 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	   *colnameTs = ssResult->temporalClause->colnameTs;
+		char	   *colnameTe = ssResult->temporalClause->colnameTe;
+		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(strcmp(usingItemName, colnameTs) == 0
+					|| strcmp(usingItemName, colnameTe) == 0
+					|| (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). These column
+ * 		names also differ depending on weither we have range types or scalars
+ * 		for temporal bounds. The list is then compared with the aliases from
+ * 		the current parser state, and renamed if necessary.
+ */
+static List *
+internalUseOnlyColumnNames(ParseState *pstate,
+						   bool hasRangeTypes,
+						   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"));
+
+	/* We split range types into upper and lower bounds, called ts and te */
+	if(hasRangeTypes)
+	{
+		filter = lappend(filter, makeString("ts"));
+		filter = lappend(filter, makeString("te"));
+	}
+
+	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. This means, if
+ * 		we have only one item in boundary list, all bounds must be range types.
+ * 		Otherwise, all bounds must be scalars.
+ */
+static void
+temporalBoundCheckIntegrity(ParseState *pstate,
+							 List *bounds,
+							 List *colnames,
+							 List *colvars,
+							 TemporalType tmpType)
+{
+	ListCell 	*lcNames;
+	ListCell 	*lcVars;
+	ListCell 	*lcBound;
+	ListCell 	*lcFilter;
+	bool 		 hasRangeTypes = list_length(bounds) == 1;
+	List		*filter = internalUseOnlyColumnNames(pstate,
+													 hasRangeTypes,
+													 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(hasRangeTypes && type->typtype != TYPTYPE_RANGE)
+					msg = "Invalid column type \"%s\" for the temporal bound " \
+						  "\"%s\". It must be a range type column.";
+
+				if(! hasRangeTypes && type->typtype == TYPTYPE_RANGE)
+					msg = "Invalid column type \"%s\" for the temporal bound " \
+						  "\"%s\". It must be a scalar type column (i.e., " \
+						  "not a range type).";
+
+				if (strlen(msg) > 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+							 errmsg(msg,
+									NameStr(type->typname),
+									NameListToString(crb->fields)),
+							 errhint("Specify four scalar columns for the " \
+									 "temporal boundaries, or 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;
+	bool 		foundTS = false;
+	bool 		foundTE = false;
+	int 		counterP1 = -1;
+	int 		counterP2 = -1;
+	int 		counterRN = -1;
+	int 		counterTS = -1;
+	int 		counterTE = -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);
+
+		if (j->inTmpPrimHasRangeT)
+		{
+			getColumnCounter(colname, "ts", &foundTS, &counterTS);
+			getColumnCounter(colname, "te", &foundTE, &counterTE);
+		}
+	}
+
+	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 (j->inTmpPrimHasRangeT)
+		{
+			getColumnCounter(colname, "ts", &foundTS, &counterTS);
+			getColumnCounter(colname, "te", &foundTE, &counterTE);
+		}
+	}
+
+	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;
+			A_Expr *e2;
+			List *qualArgs;
+			bool hasRangeTypes = list_length(j->temporalBounds) == 2;
+
+			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(! hasRangeTypes)
+			{
+				e2 = (A_Expr *) lsecond(qualArgs);
+				linitial(((ColumnRef *)e2->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);
+
+	if (foundTS)
+		addTemporalAlias(pstate, "ts", counterTS);
+
+	if (foundTE)
+		addTemporalAlias(pstate, "te", counterTE);
+
+	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;
+	j->inTmpPrimHasRangeT = list_length(bounds) == 2;
+
+	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;
+	j->inTmpPrimHasRangeT = list_length(bounds) == 2;
+
+	return j;
+}
+
diff --git src/backend/utils/adt/windowfuncs.c src/backend/utils/adt/windowfuncs.c
index 3c1d3cf..e22814b 100644
--- src/backend/utils/adt/windowfuncs.c
+++ 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 src/backend/utils/errcodes.txt src/backend/utils/errcodes.txt
index e7bdb92..ee42cee 100644
--- src/backend/utils/errcodes.txt
+++ src/backend/utils/errcodes.txt
@@ -204,6 +204,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 src/include/catalog/pg_proc.h src/include/catalog/pg_proc.h
index 047a1ce..8aab18b 100644
--- src/include/catalog/pg_proc.h
+++ src/include/catalog/pg_proc.h
@@ -4989,6 +4989,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 = 3999 (  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 src/include/executor/nodeTemporalAdjustment.h src/include/executor/nodeTemporalAdjustment.h
new file mode 100644
index 0000000..7a4be3d
--- /dev/null
+++ 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 src/include/nodes/execnodes.h src/include/nodes/execnodes.h
index 8004d85..795afe3 100644
--- src/include/nodes/execnodes.h
+++ src/include/nodes/execnodes.h
@@ -1259,6 +1259,30 @@ 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 */
+	Form_pg_attribute     datumFormat;	  /* Datum format of sweepline, P1, P2 */
+} TemporalAdjustmentState;
+
+/* ----------------
  *	 SeqScanState information
  * ----------------
  */
diff --git src/include/nodes/makefuncs.h src/include/nodes/makefuncs.h
index 47500cb..05d8587 100644
--- src/include/nodes/makefuncs.h
+++ 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 src/include/nodes/nodes.h src/include/nodes/nodes.h
index cb9307c..dff2565 100644
--- src/include/nodes/nodes.h
+++ src/include/nodes/nodes.h
@@ -79,6 +79,7 @@ typedef enum NodeTag
 	T_SetOp,
 	T_LockRows,
 	T_Limit,
+	T_TemporalAdjustment,
 	/* these aren't subclasses of Plan: */
 	T_NestLoopParam,
 	T_PlanRowMark,
@@ -127,6 +128,7 @@ typedef enum NodeTag
 	T_SetOpState,
 	T_LockRowsState,
 	T_LimitState,
+	T_TemporalAdjustmentState,
 
 	/*
 	 * TAGS FOR PRIMITIVE NODES (primnodes.h)
@@ -257,6 +259,7 @@ typedef enum NodeTag
 	T_LockRowsPath,
 	T_ModifyTablePath,
 	T_LimitPath,
+	T_TemporalAdjustmentPath,
 	/* these aren't subclasses of Path: */
 	T_EquivalenceClass,
 	T_EquivalenceMember,
@@ -454,6 +457,7 @@ typedef enum NodeTag
 	T_CommonTableExpr,
 	T_RoleSpec,
 	T_TriggerTransition,
+	T_TemporalClause,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
@@ -656,7 +660,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 src/include/nodes/parsenodes.h src/include/nodes/parsenodes.h
index f8003e4..960b241 100644
--- src/include/nodes/parsenodes.h
+++ src/include/nodes/parsenodes.h
@@ -162,6 +162,8 @@ typedef struct Query
 										 * are only added during rewrite and
 										 * therefore are not written out as
 										 * part of Query. */
+
+	Node	   *temporalClause; /* temporal primitive node */
 } Query;
 
 
@@ -1337,6 +1339,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 src/include/nodes/plannodes.h src/include/nodes/plannodes.h
index e2fbc7d..b30dd5f 100644
--- src/include/nodes/plannodes.h
+++ src/include/nodes/plannodes.h
@@ -200,6 +200,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 src/include/nodes/primnodes.h src/include/nodes/primnodes.h
index 65510b0..e1e09c8 100644
--- src/include/nodes/primnodes.h
+++ src/include/nodes/primnodes.h
@@ -58,6 +58,35 @@ 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   attNumTs;
+	AttrNumber   attNumTe;
+	AttrNumber   attNumTr;
+	AttrNumber   attNumP1;
+	AttrNumber   attNumP2;
+	AttrNumber   attNumRN;
+
+	char        *colnameTs;
+	char        *colnameTe;
+	char		*colnameTr;	    /* If range type used for bounds, or NULL */
+} TemporalClause;
+
 /*
  * RangeVar - range variable, used in FROM clauses
  *
@@ -1422,6 +1451,10 @@ 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 */
+	bool		 inTmpPrimHasRangeT; /* true, if bounds are range types */
 } JoinExpr;
 
 /*----------
diff --git src/include/nodes/print.h src/include/nodes/print.h
index 431d72d..2f875c0 100644
--- src/include/nodes/print.h
+++ 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 src/include/nodes/relation.h src/include/nodes/relation.h
index 3a1255a..70d8fe7 100644
--- src/include/nodes/relation.h
+++ src/include/nodes/relation.h
@@ -1047,6 +1047,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 src/include/optimizer/pathnode.h src/include/optimizer/pathnode.h
index 71d9154..17094f9 100644
--- src/include/optimizer/pathnode.h
+++ src/include/optimizer/pathnode.h
@@ -149,6 +149,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 src/include/parser/kwlist.h src/include/parser/kwlist.h
index 77d873b..28c31ee 100644
--- src/include/parser/kwlist.h
+++ 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)
@@ -255,6 +256,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 src/include/parser/parse_node.h src/include/parser/parse_node.h
index 6633586..dd33f58 100644
--- src/include/parser/parse_node.h
+++ src/include/parser/parse_node.h
@@ -159,6 +159,12 @@ struct ParseState
 	RangeTblEntry *p_target_rangetblentry;
 
 	/*
+	 * 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 src/include/parser/parse_temporal.h src/include/parser/parse_temporal.h
new file mode 100644
index 0000000..235831e
--- /dev/null
+++ 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 src/include/utils/builtins.h src/include/utils/builtins.h
index 90f5132..15c2eef 100644
--- src/include/utils/builtins.h
+++ src/include/utils/builtins.h
@@ -1244,6 +1244,7 @@ extern Datum uuid_hash(PG_FUNCTION_ARGS);
 
 /* windowfuncs.c */
 extern Datum window_row_number(PG_FUNCTION_ARGS);
+extern Datum window_row_id(PG_FUNCTION_ARGS);
 extern Datum window_rank(PG_FUNCTION_ARGS);
 extern Datum window_dense_rank(PG_FUNCTION_ARGS);
 extern Datum window_percent_rank(PG_FUNCTION_ARGS);
#8David Fetter
david@fetter.org
In reply to: Peter Moser (#7)
Re: [PROPOSAL] Temporal query processing with range types

On Wed, Dec 07, 2016 at 03:57:33PM +0100, Peter Moser wrote:

Am 05.12.2016 um 06:11 schrieb Haribabu Kommi:

On Tue, Oct 25, 2016 at 8:44 PM, Peter Moser <pitiz29a@gmail.com
<mailto:pitiz29a@gmail.com>> wrote:

We decided to follow your recommendation and add the patch to the
commitfest.

Path is not applying properly to HEAD.
Moved to next CF with "waiting on author" status.

We updated our patch. We tested it with the latest
commit dfe530a09226a9de80f2b4c3d5f667bf51481c49.

This looks neat, but it no longer applies to master. Is a rebase in
the offing?

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david(dot)fetter(at)gmail(dot)com

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#9Peter Moser
pitiz29a@gmail.com
In reply to: David Fetter (#8)
1 attachment(s)
Re: [PROPOSAL] Temporal query processing with range types

Am 16.12.2016 um 07:17 schrieb David Fetter:

On Wed, Dec 07, 2016 at 03:57:33PM +0100, Peter Moser wrote:

Am 05.12.2016 um 06:11 schrieb Haribabu Kommi:

On Tue, Oct 25, 2016 at 8:44 PM, Peter Moser <pitiz29a@gmail.com
<mailto:pitiz29a@gmail.com>> wrote:

We decided to follow your recommendation and add the patch to the
commitfest.

Path is not applying properly to HEAD.
Moved to next CF with "waiting on author" status.

We updated our patch. We tested it with the latest
commit dfe530a09226a9de80f2b4c3d5f667bf51481c49.

This looks neat, but it no longer applies to master. Is a rebase in
the offing?

We rebased our patch on top of HEAD, that is, commit
93513d1b6559b2d0805f0b02d312ee550e3d010b.

Best regards,
Anton, Johann, Michael, Peter

Attachments:

tpg_primitives_out_v3.patchtext/x-patch; name=tpg_primitives_out_v3.patchDownload
diff --git src/backend/commands/explain.c src/backend/commands/explain.c
index 0a669d9..09406d4 100644
--- src/backend/commands/explain.c
+++ src/backend/commands/explain.c
@@ -875,6 +875,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 src/backend/executor/Makefile src/backend/executor/Makefile
index 51edd4c..42801d3 100644
--- src/backend/executor/Makefile
+++ src/backend/executor/Makefile
@@ -25,6 +25,8 @@ OBJS = execAmi.o execCurrent.o execGrouping.o execIndexing.o execJunk.o \
        nodeSamplescan.o nodeSeqscan.o nodeSetOp.o nodeSort.o nodeUnique.o \
        nodeValuesscan.o nodeCtescan.o nodeWorktablescan.o \
        nodeGroup.o nodeSubplan.o nodeSubqueryscan.o nodeTidscan.o \
-       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o
+       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o \
+       nodeTemporalAdjustment.o
+
 
 include $(top_srcdir)/src/backend/common.mk
diff --git src/backend/executor/execProcnode.c src/backend/executor/execProcnode.c
index 554244f..610d753 100644
--- src/backend/executor/execProcnode.c
+++ src/backend/executor/execProcnode.c
@@ -114,6 +114,7 @@
 #include "executor/nodeValuesscan.h"
 #include "executor/nodeWindowAgg.h"
 #include "executor/nodeWorktablescan.h"
+#include "executor/nodeTemporalAdjustment.h"
 #include "nodes/nodeFuncs.h"
 #include "miscadmin.h"
 
@@ -334,6 +335,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 */
@@ -531,6 +537,10 @@ ExecProcNode(PlanState *node)
 			result = ExecLimit((LimitState *) node);
 			break;
 
+		case T_TemporalAdjustmentState:
+			result = ExecTemporalAdjustment((TemporalAdjustmentState *) node);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
 			result = NULL;
@@ -779,6 +789,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;
@@ -812,3 +826,4 @@ ExecShutdownNode(PlanState *node)
 
 	return planstate_tree_walker(node, ExecShutdownNode, NULL);
 }
+
diff --git src/backend/executor/nodeTemporalAdjustment.c src/backend/executor/nodeTemporalAdjustment.c
new file mode 100644
index 0000000..95d58a3
--- /dev/null
+++ src/backend/executor/nodeTemporalAdjustment.c
@@ -0,0 +1,537 @@
+#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 */
+
+/*
+ * #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);
+}
+
+/*
+ * 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->attNumTs - 1] = newTs;
+	node->newValues[node->temporalCl->attNumTe - 1] = newTe;
+	if(node->temporalCl->attNumTr != -1)
+		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.ts, r.te, s.ts, s.te)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1, P2)
+ * 	  where r_1,...,r_n are all attributes from relation r. Two of these
+ * 	  attributes are temporal boundaries, namely TS and TE. The interval
+ * 	  [TS,TE) 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.ts, r.te, s.ts, s.te)) x
+ * 	  or
+ * 	  ... FROM (r NORMALIZE s ON theta WITH (r.ts, r.te, s.ts, s.te)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1)
+ * 	  where r_1,...,r_n are all attributes from relation r. Two of these
+ * 	  attributes are temporal boundaries, namely TS and TE. The interval
+ * 	  [TS,TE) represents the VALID TIME of each tuple. RN is the
+ * 	  temporal group ID or row number, and P1 is union of both
+ * 	  timepoints TS and TE 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;
+	Datum 				 currP2;
+	Datum				 currRN;
+	Datum				 prevRN;
+	Datum				 prevTe;
+
+	if(node->firstCall)
+	{
+		curr = ExecProcNode(outerPlan);
+		if(TupIsNull(curr))
+			return NULL;
+
+		prev = ExecCopySlot(prev, curr);
+		node->sameleft = true;
+		node->firstCall = false;
+		node->outrn = 0;
+		node->datumFormat = curr->tts_tupleDescriptor->attrs[tc->attNumTs - 1];
+		setSweepline(slotGetAttrNotNull(curr, tc->attNumTs));
+	}
+
+	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(heapGetAttrNotNull(out, tc->attNumTs),
+									currP1,
+									node)
+						|| !isEqual(heapGetAttrNotNull(out, tc->attNumTe),
+									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 = heapGetAttrNotNull(prev, tc->attNumTe);
+
+			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(slotGetAttrNotNull(curr, tc->attNumTs));
+			}
+			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));
+
+	/* 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;
+
+	/*
+	 * Miscellaneous initialization
+	 *
+	 * Unique 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.
+	 */
+	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 align nodes do no projections, so initialize projection info for
+	 * this node appropriately
+	 */
+	ExecAssignResultTypeFromTL(&state->ss.ps);
+	state->ss.ps.ps_ProjInfo = NULL;
+	state->ss.ps.ps_TupFromTlist = false;
+
+	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->attNumTs - 1] = true;
+	state->tsteMask[state->temporalCl->attNumTe - 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);
+
+	/*
+	 * Range types in boundaries need special treatment:
+	 * - there is an extra column in each tuple that must be changed
+	 * - and a range constructor method that must be called
+	 */
+	if(node->temporalCl->attNumTr != -1)
+	{
+		state->tsteMask[state->temporalCl->attNumTr - 1] = true;
+		fmgr_info(fmgr_internal_function("range_constructor2"), rcFunctionInfo);
+		rcFunctionInfo->fn_expr = (fmNodePtr) node->rangeVar;
+		InitFunctionCallInfoData(state->rcFuncCallInfo, rcFunctionInfo, 2,
+								 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));
+}
+
+
+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 src/backend/nodes/copyfuncs.c src/backend/nodes/copyfuncs.c
index d973225..14c02e9 100644
--- src/backend/nodes/copyfuncs.c
+++ src/backend/nodes/copyfuncs.c
@@ -1949,6 +1949,9 @@ _copyJoinExpr(const JoinExpr *from)
 	COPY_NODE_FIELD(quals);
 	COPY_NODE_FIELD(alias);
 	COPY_SCALAR_FIELD(rtindex);
+	COPY_NODE_FIELD(temporalBounds);
+	COPY_SCALAR_FIELD(inTmpPrimTempType);
+	COPY_SCALAR_FIELD(inTmpPrimHasRangeT);
 
 	return newnode;
 }
@@ -2308,6 +2311,45 @@ _copyOnConflictClause(const OnConflictClause *from)
 	return newnode;
 }
 
+static TemporalClause *
+_copyTemporalClause(const TemporalClause *from)
+{
+	TemporalClause *newnode = makeNode(TemporalClause);
+
+	COPY_SCALAR_FIELD(temporalType);
+	COPY_SCALAR_FIELD(attNumTs);
+	COPY_SCALAR_FIELD(attNumTe);
+	COPY_SCALAR_FIELD(attNumTr);
+	COPY_SCALAR_FIELD(attNumP1);
+	COPY_SCALAR_FIELD(attNumP2);
+	COPY_SCALAR_FIELD(attNumRN);
+	COPY_STRING_FIELD(colnameTs);
+	COPY_STRING_FIELD(colnameTe);
+	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)
 {
@@ -2767,6 +2809,7 @@ _copyQuery(const Query *from)
 	COPY_NODE_FIELD(setOperations);
 	COPY_NODE_FIELD(constraintDeps);
 	COPY_NODE_FIELD(withCheckOptions);
+	COPY_NODE_FIELD(temporalClause);
 
 	return newnode;
 }
@@ -2834,6 +2877,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);
@@ -5167,6 +5211,12 @@ copyObject(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 src/backend/nodes/equalfuncs.c src/backend/nodes/equalfuncs.c
index edc1797..db36e77 100644
--- src/backend/nodes/equalfuncs.c
+++ src/backend/nodes/equalfuncs.c
@@ -755,6 +755,9 @@ _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);
+	COMPARE_SCALAR_FIELD(inTmpPrimHasRangeT);
 
 	return true;
 }
diff --git src/backend/nodes/makefuncs.c src/backend/nodes/makefuncs.c
index 20e2dbd..ff6feab 100644
--- src/backend/nodes/makefuncs.c
+++ 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 -
@@ -610,3 +693,4 @@ makeGroupingSet(GroupingSetKind kind, List *content, int location)
 	n->location = location;
 	return n;
 }
+
diff --git src/backend/nodes/outfuncs.c src/backend/nodes/outfuncs.c
index 7258c03..696bfb2 100644
--- src/backend/nodes/outfuncs.c
+++ src/backend/nodes/outfuncs.c
@@ -1548,6 +1548,9 @@ _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);
+	WRITE_BOOL_FIELD(inTmpPrimHasRangeT);
 }
 
 static void
@@ -1816,6 +1819,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");
@@ -2498,6 +2513,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);
@@ -2574,6 +2590,38 @@ _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(attNumTs);
+	WRITE_INT_FIELD(attNumTe);
+	WRITE_INT_FIELD(attNumTr);
+	WRITE_INT_FIELD(attNumP1);
+	WRITE_INT_FIELD(attNumP2);
+	WRITE_INT_FIELD(attNumRN);
+	WRITE_STRING_FIELD(colnameTs);
+	WRITE_STRING_FIELD(colnameTe);
+	WRITE_STRING_FIELD(colnameTr);
+}
+
+static void
 _outColumnDef(StringInfo str, const ColumnDef *node)
 {
 	WRITE_NODE_TYPE("COLUMNDEF");
@@ -2705,6 +2753,7 @@ _outQuery(StringInfo str, const Query *node)
 	WRITE_NODE_FIELD(rowMarks);
 	WRITE_NODE_FIELD(setOperations);
 	WRITE_NODE_FIELD(constraintDeps);
+	WRITE_NODE_FIELD(temporalClause);
 }
 
 static void
@@ -3671,6 +3720,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;
@@ -3768,6 +3820,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);
@@ -3924,7 +3982,6 @@ outNode(StringInfo str, const void *obj)
 				break;
 
 			default:
-
 				/*
 				 * This should be an ERROR, but it's too useful to be able to
 				 * dump structures that outNode only understands part of.
diff --git src/backend/nodes/print.c src/backend/nodes/print.c
index a1f2941..ff24370 100644
--- src/backend/nodes/print.c
+++ src/backend/nodes/print.c
@@ -25,7 +25,7 @@
 #include "optimizer/clauses.h"
 #include "parser/parsetree.h"
 #include "utils/lsyscache.h"
-
+#include "parser/parse_node.h"
 
 /*
  * print
@@ -492,3 +492,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 src/backend/nodes/readfuncs.c src/backend/nodes/readfuncs.c
index d608530..de52ebf 100644
--- src/backend/nodes/readfuncs.c
+++ 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);
 
 	READ_DONE();
 }
@@ -423,6 +424,28 @@ _readSetOperationStmt(void)
 	READ_DONE();
 }
 
+/*
+ * _readTemporalClause
+ */
+static TemporalClause *
+_readTemporalClause(void)
+{
+	READ_LOCALS(TemporalClause);
+
+	READ_ENUM_FIELD(temporalType, TemporalType);
+	READ_INT_FIELD(attNumTs);
+	READ_INT_FIELD(attNumTe);
+	READ_INT_FIELD(attNumTr);
+	READ_INT_FIELD(attNumP1);
+	READ_INT_FIELD(attNumP2);
+	READ_INT_FIELD(attNumRN);
+	READ_STRING_FIELD(colnameTs);
+	READ_STRING_FIELD(colnameTe);
+	READ_STRING_FIELD(colnameTr);
+
+	READ_DONE();
+}
+
 
 /*
  *	Stuff from primnodes.h.
@@ -457,6 +480,17 @@ _readRangeVar(void)
 	READ_DONE();
 }
 
+static ColumnRef *
+_readColumnRef(void)
+{
+	READ_LOCALS(ColumnRef);
+
+	READ_NODE_FIELD(fields);
+	READ_LOCATION_FIELD(location);
+
+	READ_DONE();
+}
+
 static IntoClause *
 _readIntoClause(void)
 {
@@ -1238,6 +1272,9 @@ _readJoinExpr(void)
 	READ_NODE_FIELD(quals);
 	READ_NODE_FIELD(alias);
 	READ_INT_FIELD(rtindex);
+	READ_NODE_FIELD(temporalBounds);
+	READ_ENUM_FIELD(inTmpPrimTempType, TemporalType);
+	READ_BOOL_FIELD(inTmpPrimHasRangeT);
 
 	READ_DONE();
 }
@@ -2439,6 +2476,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 src/backend/optimizer/path/allpaths.c src/backend/optimizer/path/allpaths.c
index 9753a26..077c502 100644
--- src/backend/optimizer/path/allpaths.c
+++ 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 */
@@ -126,6 +127,7 @@ static void subquery_push_qual(Query *subquery,
 static void recurse_push_qual(Node *setOp, Query *topquery,
 				  RangeTblEntry *rte, Index rti, Node *qual);
 static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
+static bool allWindowFuncsHaveRowId(List *targetList);
 
 
 /*
@@ -2307,7 +2309,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;
 
 	/*
@@ -2417,6 +2420,7 @@ static void
 check_output_expressions(Query *subquery, pushdown_safety_info *safetyInfo)
 {
 	ListCell   *lc;
+	bool wfsafe = allWindowFuncsHaveRowId(subquery->targetList);
 
 	foreach(lc, subquery->targetList)
 	{
@@ -2455,12 +2459,35 @@ 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 resnoTs =
+					((TemporalClause *)subquery->temporalClause)->attNumTs;
+			AttrNumber resnoTe =
+					((TemporalClause *)subquery->temporalClause)->attNumTe;
+			AttrNumber resnoRangeT =
+					((TemporalClause *)subquery->temporalClause)->attNumTr;
+
+			if (tle->resno == resnoTs
+					|| tle->resno == resnoTe
+					|| tle->resno == resnoRangeT)
+			{
+				safetyInfo->unsafeColumns[tle->resno] = true;
+				continue;
+			}
+		}
 	}
 }
 
@@ -2530,6 +2557,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
@@ -2813,6 +2866,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 src/backend/optimizer/plan/createplan.c src/backend/optimizer/plan/createplan.c
index ad49674..a3f590f 100644
--- src/backend/optimizer/plan/createplan.c
+++ src/backend/optimizer/plan/createplan.c
@@ -113,6 +113,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,
@@ -270,6 +273,9 @@ static ModifyTable *make_modifytable(PlannerInfo *root,
 				 List *resultRelations, List *subplans,
 				 List *withCheckOptionLists, List *returningLists,
 				 List *rowMarks, OnConflictExpr *onconflict, int epqParam);
+static TemporalAdjustment *make_temporalAdjustment(Plan *lefttree,
+						TemporalClause *temporalClause,
+						List *sortClause);
 
 
 /*
@@ -463,6 +469,11 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
 											  (LimitPath *) best_path,
 											  flags);
 			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);
@@ -2246,6 +2257,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;
+}
+
 
 /*****************************************************************************
  *
@@ -4826,6 +4864,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 = 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 src/backend/optimizer/plan/planner.c src/backend/optimizer/plan/planner.c
index 41dde50..0f76232 100644
--- src/backend/optimizer/plan/planner.c
+++ src/backend/optimizer/plan/planner.c
@@ -1987,6 +1987,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
@@ -4358,7 +4372,6 @@ create_ordered_paths(PlannerInfo *root,
 	return ordered_rel;
 }
 
-
 /*
  * make_group_input_target
  *	  Generate appropriate PathTarget for initial input to grouping nodes.
diff --git src/backend/optimizer/plan/setrefs.c src/backend/optimizer/plan/setrefs.c
index 2fe1c8c..0ff19aa 100644
--- src/backend/optimizer/plan/setrefs.c
+++ src/backend/optimizer/plan/setrefs.c
@@ -612,6 +612,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 src/backend/optimizer/plan/subselect.c src/backend/optimizer/plan/subselect.c
index 3171743..a1c9733 100644
--- src/backend/optimizer/plan/subselect.c
+++ src/backend/optimizer/plan/subselect.c
@@ -2687,6 +2687,7 @@ finalize_plan(PlannerInfo *root, Plan *plan, Bitmapset *valid_params,
 		case T_Gather:
 		case T_SetOp:
 		case T_Group:
+		case T_TemporalAdjustment:
 			break;
 
 		default:
diff --git src/backend/optimizer/util/pathnode.c src/backend/optimizer/util/pathnode.c
index 6d3ccfd..e6f92a0 100644
--- src/backend/optimizer/util/pathnode.c
+++ src/backend/optimizer/util/pathnode.c
@@ -2362,6 +2362,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 src/backend/parser/Makefile src/backend/parser/Makefile
index fdd8485..35c20fa 100644
--- src/backend/parser/Makefile
+++ src/backend/parser/Makefile
@@ -15,7 +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_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_relation.o parse_target.o parse_type.o parse_utilcmd.o scansup.o \
+      parse_temporal.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git src/backend/parser/analyze.c src/backend/parser/analyze.c
index 5e65fe7..45f3ac2 100644
--- src/backend/parser/analyze.c
+++ 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"
@@ -1188,6 +1189,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");
@@ -1262,6 +1266,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 src/backend/parser/gram.y src/backend/parser/gram.y
index 2ed7b52..e600840 100644
--- src/backend/parser/gram.y
+++ src/backend/parser/gram.y
@@ -410,6 +410,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
@@ -461,11 +462,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
@@ -559,6 +563,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>		partbound_datum_list
 %type <partrange_datum>	PartitionRangeDatum
 %type <list>		range_datum_list
+%type <list>    temporal_bounds_list
+
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -583,7 +589,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
@@ -629,7 +635,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
 
@@ -10816,6 +10822,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.
@@ -11072,6 +11091,94 @@ 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 period boundaries with 4 comparable values
+					 * or two range typed values,
+					 * i.e. [lts, lte) is the left argument period, and
+					 *		[rts, rte) is the right argument period,
+					 * or two compatible range types with bounds like '[)'
+					 */
+					if(list_length($6) == 4 || list_length($6) == 2)
+						n->temporalBounds = $6;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have 2 range typed values, or four " \
+										"single 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 period boundaries with 4 comparable values
+					 * or two range typed values,
+					 * i.e. [lts, lte) is the left argument period, and
+					 *      [rts, rte) is the right argument period,
+					 * or two compatible range types with bounds like '[)'
+					 */
+					if(list_length($5) == 4 || list_length($5) == 2)
+						n->temporalBounds = $5;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have 2 range typed values, or four " \
+										"single values."),
+								 parser_errposition(@5)));
+
+					$$ = n;
+				}
 		;
 
 
@@ -14382,7 +14489,8 @@ type_func_name_keyword:
  * forced to.
  */
 reserved_keyword:
-			  ALL
+			  ALIGN
+			| ALL
 			| ANALYSE
 			| ANALYZE
 			| AND
@@ -14430,6 +14538,7 @@ reserved_keyword:
 			| LIMIT
 			| LOCALTIME
 			| LOCALTIMESTAMP
+			| NORMALIZE
 			| NOT
 			| NULL_P
 			| OFFSET
diff --git src/backend/parser/parse_clause.c src/backend/parser/parse_clause.c
index 751de4b..47d3391 100644
--- src/backend/parser/parse_clause.c
+++ src/backend/parser/parse_clause.c
@@ -39,6 +39,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"
@@ -967,6 +968,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.
 		 */
@@ -1029,6 +1067,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 src/backend/parser/parse_temporal.c src/backend/parser/parse_temporal.c
new file mode 100644
index 0000000..3a3f86f
--- /dev/null
+++ src/backend/parser/parse_temporal.c
@@ -0,0 +1,1620 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 can have four elements in a boundary
+ * list (i.e., WITH-clause of a temporal primitive) when we have two argument
+ * relations with scalar boundaries, or two entries if we have 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_LARGTST = 0,
+	TPB_LARGTE,
+	TPB_RARGTST,
+	TPB_RARGTE
+} 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,
+						  bool *hasRangeTypes,
+						  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,
+						   bool hasRangeTypes,
+						   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. These are either
+ * 		two scalar values for TS and TE, or a single range type value T holding
+ * 		both bounds.
+ */
+static List *
+temporalBoundGetLeftBounds(List *bounds)
+{
+	switch(list_length(bounds))
+	{
+		case 2: return list_make1(linitial(bounds));
+		case 4: return list_make2(linitial(bounds), lsecond(bounds));
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT),
+			 errmsg("Invalid temporal bound list length."),
+			 errhint("Specify four scalar columns for the " \
+					 "temporal boundaries, or two range-typed "\
+					 "columns.")));
+
+	/* Keep compiler quiet */
+	return NIL;
+}
+
+/*
+ * temporalBoundGetRightBounds -
+ * 		Return the right boundaries of a temporal bounds list. These are either
+ * 		two scalar values for TS and TE, or a single range type value T holding
+ * 		both bounds.
+ */
+static List *
+temporalBoundGetRightBounds(List *bounds)
+{
+	switch(list_length(bounds))
+	{
+		case 2: return list_make1(lsecond(bounds));
+		case 4: return list_make2(lthird(bounds), lfourth(bounds));
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT),
+			 errmsg("Invalid temporal bound list length."),
+			 errhint("Specify four scalar columns for the " \
+					 "temporal boundaries, or two range-typed "\
+					 "columns.")));
+
+	/* Keep compiler quiet */
+	return NIL;
+}
+
+/*
+ * 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)
+	{
+		/*
+		 * Four boundary entries means that we have 2x two scalar boundaries.
+		 * Which means the first two entries are start and end of the first
+		 * bound, and the 3th and 4th entry are start and end of the second
+		 * bound.
+		 */
+		case 4:
+			return list_nth(bounds, id);
+
+		/*
+		 * 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_LARGTST)
+				return linitial(bounds);
+			if(id == TPB_RARGTST || id == TPB_LARGTE)
+				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_LARGTST)
+				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 four scalar columns for the " \
+						 "temporal boundaries, or two range-typed "\
+						 "columns.")));
+
+	return NULL;
+}
+
+/*
+ * transformTemporalClause -
+ * 		If we have a temporal primitive query, we must find all attribute
+ * 		numbers for p1, p2, rn, ts, te, 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, all attribute numbers for temporal boundaries are unknown */
+	tc->attNumTr = -1;
+	tc->attNumTe = -1;
+	tc->attNumTs = -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;
+	}
+
+	/*
+	 * If we have range types the subquery splits it into separate
+	 * columns, called ts and te which are in between the p1- and
+	 * rn-column.
+	 */
+	if(tc->colnameTr)
+	{
+		tc->attNumTe = pos--;
+		tc->attNumTs = pos--;
+	}
+
+	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 if(strcmp(key, "ts") == 0)
+		{
+			tc->colnameTs = pstrdup(value);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumTs);
+			foundTsTe = true;
+		}
+		else if(strcmp(key, "te") == 0)
+		{
+			tc->colnameTe = pstrdup(value);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumTe);
+			foundTsTe = true;
+		}
+		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;
+
+		/* Temporal boundary is a range type */
+		if (tc->colnameTr)
+		{
+			if (strcmp(tle->resname, tc->colnameTr) == 0)
+				tc->attNumTr = tle->resno;
+		}
+		else /* Two scalar columns for boundaries */
+		{
+			if (strcmp(tle->resname, tc->colnameTs) == 0)
+				tc->attNumTs = tle->resno;
+			else if (strcmp(tle->resname, tc->colnameTe) == 0)
+				tc->attNumTe = tle->resno;
+		}
+	}
+
+	/* We need column attribute numbers for all temporal boundaries */
+	if(tc->attNumTs == -1
+			|| tc->attNumTe == -1
+			|| (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;
+
+	/* We use range types, remove splitted columns, i.e. upper/lower bounds */
+	if(tc->colnameTr) {
+		get_tle_by_resno(qry->targetList, tc->attNumTs)->resjunk = true;
+		get_tle_by_resno(qry->targetList, tc->attNumTe)->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, bool *hasRangeTypes, 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;
+
+	/*
+	 * We can have 2 or 4 column references, i.e. if we have 4, the first two
+	 * form the left argument period [largTs, largTe), and the last two the
+	 * right argument period respectively. Otherwise, we have two range typed
+	 * values of the form '[)' where the first argument contains the boundaries
+	 * of the left-hand-side, and the second argument contains the boundaries
+	 * of the RHS respectively. The parser checked already if there was another
+	 * number of arguments (not equal to 2 or 4) given.
+	 */
+	*hasRangeTypes = list_length(j->temporalBounds) == 2;
+
+	/*
+	 * 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);
+	if(*hasRangeTypes)
+	{
+		/*
+		 * Hardcoded column names for ts and te. We handle ambiguous column
+		 * names during the transformation of temporal primitive clauses.
+		 */
+		ssResult->temporalClause->colnameTs = "ts";
+		ssResult->temporalClause->colnameTe = "te";
+		ssResult->temporalClause->colnameTr =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTST);
+	}
+	else
+	{
+		ssResult->temporalClause->colnameTs =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTST);
+		ssResult->temporalClause->colnameTe =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTE);
+		ssResult->temporalClause->colnameTr = NULL;
+	}
+
+	/*
+	 * 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;
+	joinExpr->inTmpPrimHasRangeT = *hasRangeTypes;
+
+	return ssResult;
+}
+
+/*
+ * transformTemporalAligner -
+ * 		transform a TEMPORAL ALIGN clause into standard SQL
+ *
+ * INPUT:
+ * 		(r ALIGN s ON q WITH (r.ts, r.te, s.ts, s.te)) c
+ * 		where q can be any join qualifier, and r.ts, r.te, s.ts, and s.te
+ * 		can be any column name.
+ *
+ * OUTPUT:
+ * 		(
+ * 		SELECT r.*, GREATEST(r.ts, s.ts) P1, LEAST(r.te, s.te) P2
+ *      FROM
+ *      (
+ *      	SELECT *, row_id() OVER () rn FROM r
+ *      ) r
+ *      LEFT OUTER JOIN
+ *      s
+ *      ON q AND r.ts < s.te AND r.te > s.ts
+ *      ORDER BY rn, P1, P2
+ *      ) c
+ */
+Node *
+transformTemporalAligner(ParseState *pstate, JoinExpr *j)
+{
+	const int 	 		 UNKNOWN_LOCATION = -1;
+
+	bool				 hasRangeTypes;
+	SelectStmt			*ssResult;
+	RangeSubselect 		*rssResult;
+	ResTarget			*rtGreatest;
+	ResTarget			*rtLeast;
+	ResTarget			*rtLowerLarg;
+	ResTarget			*rtUpperLarg;
+	ColumnRef 			*crLargTs;
+	ColumnRef 			*crRargTs;
+	ColumnRef 			*crLargTe;
+	ColumnRef 			*crRargTe;
+	MinMaxExpr			*mmeGreatest;
+	MinMaxExpr			*mmeLeast;
+	FuncCall			*fcLowerLarg;
+	FuncCall			*fcLowerRarg;
+	FuncCall			*fcUpperLarg;
+	FuncCall			*fcUpperRarg;
+	List				*mmeGreatestArgs;
+	List				*mmeLeastArgs;
+	List				*boundariesExpr;
+	JoinExpr			*joinExpr;
+	A_Expr				*lowerBoundExpr;
+	A_Expr				*upperBoundExpr;
+	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, &hasRangeTypes,
+										 &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.
+	 */
+	if (hasRangeTypes)
+	{
+		crLargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crRargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   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);
+
+		rtLowerLarg = makeResTarget((Node *) fcLowerLarg,
+									ssResult->temporalClause->colnameTs);
+		rtUpperLarg = makeResTarget((Node *) fcUpperLarg,
+									ssResult->temporalClause->colnameTe);
+
+		ssResult->targetList = list_concat(ssResult->targetList,
+										   list_make2(rtLowerLarg, rtUpperLarg));
+	}
+	else
+	{
+		crLargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crLargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTE,
+										   largAlias->aliasname);
+		crRargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+		crRargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTE,
+										   rargAlias->aliasname);
+
+		/* Create argument list for function call to "greatest" and "least" */
+		mmeGreatestArgs = list_make2(crLargTs, crRargTs);
+		mmeLeastArgs = list_make2(crLargTe, crRargTe);
+
+		/*
+		 * Build Boolean expressions, i.e. "r.ts < s.te AND r.te > s.ts"
+		 * and concatenate it with q (=theta)
+		 */
+		lowerBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  "<",
+										  copyObject(crLargTs),
+										  copyObject(crRargTe),
+										  UNKNOWN_LOCATION);
+		upperBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  ">",
+										  copyObject(crLargTe),
+										  copyObject(crRargTs),
+										  UNKNOWN_LOCATION);
+
+		boundariesExpr = list_make2(lowerBoundExpr, upperBoundExpr);
+	}
+
+	/* 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.ts, r.te, s.ts, s.te)) c
+ *
+ * 		-- or --
+ *
+ * 		(r NORMALIZE s USING(atts) WITH (r.ts, r.te, s.ts, s.te)) c
+ * 		where q can be any join qualifier and r.ts, r.te, s.ts, and s.te
+ * 		can be any column name.
+ *
+ * OUTPUT:
+ * 		(
+ * 			SELECT r.*,
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT s.*, ts P1 FROM s
+ *      		UNION ALL
+ *      		SELECT s.*, te P1 FROM s
+ *      	) s
+ *      	ON q AND P1 >= r.ts AND P1 < r.te
+ *      	ORDER BY rn, P1
+ *      ) c
+ *
+ *      -- or --
+ *
+ * 		(
+ * 			SELECT r.*,
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT atts, ts P1 FROM s
+ *      		UNION
+ *      		SELECT atts, te P1 FROM s
+ *      	) s
+ *      	ON r.atts = s.atts AND P1 >= r.ts AND P1 < r.te
+ *      	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;
+	ResTarget			*rtLowerLarg;
+	ResTarget			*rtUpperLarg;
+	ColumnRef 			*crRargStar;
+	ColumnRef 			*crLargTsT = NULL;
+	ColumnRef 			*crRargTsT = NULL;
+	ColumnRef 			*crLargTe = NULL;
+	ColumnRef 			*crRargTe = NULL;
+	ColumnRef 			*crP1;
+	JoinExpr			*joinExpr;
+	A_Expr				*lowerBoundExpr;
+	A_Expr				*upperBoundExpr;
+	A_Expr				*containsExpr;
+	Node				*boolExpr;
+	Alias				*largAlias;
+	Alias				*rargAlias;
+	char 				*colnameRN;
+	char 				*colnameP1;
+	bool				 hasRangeTypes;
+	FuncCall			*fcLowerLarg = NULL;
+	FuncCall			*fcUpperLarg = NULL;
+	FuncCall			*fcLowerRarg = NULL;
+	FuncCall			*fcUpperRarg = NULL;
+	List				*boundariesExpr;
+
+	/* Create a select statement skeleton to be filled here */
+	ssResult = makeTemporalQuerySkeleton(j, &colnameRN, &colnameP1,
+										 NULL, &hasRangeTypes,
+										 &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. If we need only two range types
+	 * only Ts columnrefs are used.
+	 */
+	if (hasRangeTypes)
+	{
+		crLargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crRargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+
+		/* Create argument list for function call to "greatest" and "least" */
+		fcLowerLarg = makeFuncCall(SystemFuncName("lower"),
+								   list_make1(crLargTsT),
+								   UNKNOWN_LOCATION);
+		fcLowerRarg = makeFuncCall(SystemFuncName("lower"),
+								   list_make1(crRargTsT),
+								   UNKNOWN_LOCATION);
+		fcUpperLarg = makeFuncCall(SystemFuncName("upper"),
+								   list_make1(crLargTsT),
+								   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);
+
+		rtLowerLarg = makeResTarget((Node *) fcLowerLarg,
+									ssResult->temporalClause->colnameTs);
+		rtUpperLarg = makeResTarget((Node *) fcUpperLarg,
+									ssResult->temporalClause->colnameTe);
+
+		ssResult->targetList = list_concat(ssResult->targetList,
+										   list_make2(rtLowerLarg, rtUpperLarg));
+		/*
+		 * 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);
+	}
+	else
+	{
+		/*
+		 * Build column references, for use later.
+		 */
+		crLargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crLargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTE,
+										   largAlias->aliasname);
+		crRargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+		crRargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTE,
+										   rargAlias->aliasname);
+
+		/* Build resource target "ts P1" and "te P1" */
+		rtTsP1 = makeResTarget((Node *) crRargTsT, colnameP1);
+		rtTeP1 = makeResTarget((Node *) crRargTe, colnameP1);
+		/*
+		 * Build "contains" expressions, i.e. "P1 >= ts AND P1 < te"
+		 * and concatenate it with q (=theta)
+		 */
+		lowerBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  ">=",
+										  copyObject(crP1),
+										  copyObject(crLargTsT),
+										  UNKNOWN_LOCATION);
+		upperBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  "<",
+										  copyObject(crP1),
+										  copyObject(crLargTe),
+										  UNKNOWN_LOCATION);
+
+		boundariesExpr = list_make2(lowerBoundExpr, upperBoundExpr);
+	}
+
+	/*
+	 * Build "SELECT s.*, ts P1 FROM s" and "SELECT s.*, te P1 FROM s", iff we
+	 * have a ON-clause.
+	 * If we have an USING-clause with a name-list 'atts' build "SELECT atts,
+	 * ts P1 FROM s" and "SELECT atts, te 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	   *colnameTs = ssResult->temporalClause->colnameTs;
+		char	   *colnameTe = ssResult->temporalClause->colnameTe;
+		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(strcmp(usingItemName, colnameTs) == 0
+					|| strcmp(usingItemName, colnameTe) == 0
+					|| (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). These column
+ * 		names also differ depending on weither we have range types or scalars
+ * 		for temporal bounds. The list is then compared with the aliases from
+ * 		the current parser state, and renamed if necessary.
+ */
+static List *
+internalUseOnlyColumnNames(ParseState *pstate,
+						   bool hasRangeTypes,
+						   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"));
+
+	/* We split range types into upper and lower bounds, called ts and te */
+	if(hasRangeTypes)
+	{
+		filter = lappend(filter, makeString("ts"));
+		filter = lappend(filter, makeString("te"));
+	}
+
+	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. This means, if
+ * 		we have only one item in boundary list, all bounds must be range types.
+ * 		Otherwise, all bounds must be scalars.
+ */
+static void
+temporalBoundCheckIntegrity(ParseState *pstate,
+							 List *bounds,
+							 List *colnames,
+							 List *colvars,
+							 TemporalType tmpType)
+{
+	ListCell 	*lcNames;
+	ListCell 	*lcVars;
+	ListCell 	*lcBound;
+	ListCell 	*lcFilter;
+	bool 		 hasRangeTypes = list_length(bounds) == 1;
+	List		*filter = internalUseOnlyColumnNames(pstate,
+													 hasRangeTypes,
+													 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(hasRangeTypes && type->typtype != TYPTYPE_RANGE)
+					msg = "Invalid column type \"%s\" for the temporal bound " \
+						  "\"%s\". It must be a range type column.";
+
+				if(! hasRangeTypes && type->typtype == TYPTYPE_RANGE)
+					msg = "Invalid column type \"%s\" for the temporal bound " \
+						  "\"%s\". It must be a scalar type column (i.e., " \
+						  "not a range type).";
+
+				if (strlen(msg) > 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+							 errmsg(msg,
+									NameStr(type->typname),
+									NameListToString(crb->fields)),
+							 errhint("Specify four scalar columns for the " \
+									 "temporal boundaries, or 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;
+	bool 		foundTS = false;
+	bool 		foundTE = false;
+	int 		counterP1 = -1;
+	int 		counterP2 = -1;
+	int 		counterRN = -1;
+	int 		counterTS = -1;
+	int 		counterTE = -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);
+
+		if (j->inTmpPrimHasRangeT)
+		{
+			getColumnCounter(colname, "ts", &foundTS, &counterTS);
+			getColumnCounter(colname, "te", &foundTE, &counterTE);
+		}
+	}
+
+	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 (j->inTmpPrimHasRangeT)
+		{
+			getColumnCounter(colname, "ts", &foundTS, &counterTS);
+			getColumnCounter(colname, "te", &foundTE, &counterTE);
+		}
+	}
+
+	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;
+			A_Expr *e2;
+			List *qualArgs;
+			bool hasRangeTypes = list_length(j->temporalBounds) == 2;
+
+			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(! hasRangeTypes)
+			{
+				e2 = (A_Expr *) lsecond(qualArgs);
+				linitial(((ColumnRef *)e2->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);
+
+	if (foundTS)
+		addTemporalAlias(pstate, "ts", counterTS);
+
+	if (foundTE)
+		addTemporalAlias(pstate, "te", counterTE);
+
+	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;
+	j->inTmpPrimHasRangeT = list_length(bounds) == 2;
+
+	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;
+	j->inTmpPrimHasRangeT = list_length(bounds) == 2;
+
+	return j;
+}
+
diff --git src/backend/utils/adt/windowfuncs.c src/backend/utils/adt/windowfuncs.c
index 3c1d3cf..e22814b 100644
--- src/backend/utils/adt/windowfuncs.c
+++ 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 src/backend/utils/errcodes.txt src/backend/utils/errcodes.txt
index e7bdb92..ee42cee 100644
--- src/backend/utils/errcodes.txt
+++ src/backend/utils/errcodes.txt
@@ -204,6 +204,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 src/include/catalog/pg_proc.h src/include/catalog/pg_proc.h
index cd7b909..735fa4f 100644
--- src/include/catalog/pg_proc.h
+++ src/include/catalog/pg_proc.h
@@ -4991,6 +4991,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 = 3999 (  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 src/include/executor/nodeTemporalAdjustment.h src/include/executor/nodeTemporalAdjustment.h
new file mode 100644
index 0000000..7a4be3d
--- /dev/null
+++ 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 src/include/nodes/execnodes.h src/include/nodes/execnodes.h
index 1de5c81..16483bd 100644
--- src/include/nodes/execnodes.h
+++ src/include/nodes/execnodes.h
@@ -1273,6 +1273,30 @@ 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 */
+	Form_pg_attribute     datumFormat;	  /* Datum format of sweepline, P1, P2 */
+} TemporalAdjustmentState;
+
+/* ----------------
  *	 SeqScanState information
  * ----------------
  */
diff --git src/include/nodes/makefuncs.h src/include/nodes/makefuncs.h
index 47500cb..05d8587 100644
--- src/include/nodes/makefuncs.h
+++ 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 src/include/nodes/nodes.h src/include/nodes/nodes.h
index c514d3f..5eedb3b 100644
--- src/include/nodes/nodes.h
+++ src/include/nodes/nodes.h
@@ -79,6 +79,7 @@ typedef enum NodeTag
 	T_SetOp,
 	T_LockRows,
 	T_Limit,
+	T_TemporalAdjustment,
 	/* these aren't subclasses of Plan: */
 	T_NestLoopParam,
 	T_PlanRowMark,
@@ -127,6 +128,7 @@ typedef enum NodeTag
 	T_SetOpState,
 	T_LockRowsState,
 	T_LimitState,
+	T_TemporalAdjustmentState,
 
 	/*
 	 * TAGS FOR PRIMITIVE NODES (primnodes.h)
@@ -257,6 +259,7 @@ typedef enum NodeTag
 	T_LockRowsPath,
 	T_ModifyTablePath,
 	T_LimitPath,
+	T_TemporalAdjustmentPath,
 	/* these aren't subclasses of Path: */
 	T_EquivalenceClass,
 	T_EquivalenceMember,
@@ -459,6 +462,7 @@ typedef enum NodeTag
 	T_PartitionSpec,
 	T_PartitionBoundSpec,
 	T_PartitionRangeDatum,
+	T_TemporalClause,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
@@ -661,7 +665,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 src/include/nodes/parsenodes.h src/include/nodes/parsenodes.h
index fc532fb..dae2c82 100644
--- src/include/nodes/parsenodes.h
+++ src/include/nodes/parsenodes.h
@@ -162,6 +162,8 @@ typedef struct Query
 										 * are only added during rewrite and
 										 * therefore are not written out as
 										 * part of Query. */
+
+	Node	   *temporalClause; /* temporal primitive node */
 } Query;
 
 
@@ -1417,6 +1419,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 src/include/nodes/plannodes.h src/include/nodes/plannodes.h
index e2fbc7d..b30dd5f 100644
--- src/include/nodes/plannodes.h
+++ src/include/nodes/plannodes.h
@@ -200,6 +200,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 src/include/nodes/primnodes.h src/include/nodes/primnodes.h
index 65510b0..e1e09c8 100644
--- src/include/nodes/primnodes.h
+++ src/include/nodes/primnodes.h
@@ -58,6 +58,35 @@ 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   attNumTs;
+	AttrNumber   attNumTe;
+	AttrNumber   attNumTr;
+	AttrNumber   attNumP1;
+	AttrNumber   attNumP2;
+	AttrNumber   attNumRN;
+
+	char        *colnameTs;
+	char        *colnameTe;
+	char		*colnameTr;	    /* If range type used for bounds, or NULL */
+} TemporalClause;
+
 /*
  * RangeVar - range variable, used in FROM clauses
  *
@@ -1422,6 +1451,10 @@ 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 */
+	bool		 inTmpPrimHasRangeT; /* true, if bounds are range types */
 } JoinExpr;
 
 /*----------
diff --git src/include/nodes/print.h src/include/nodes/print.h
index 431d72d..2f875c0 100644
--- src/include/nodes/print.h
+++ 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 src/include/nodes/relation.h src/include/nodes/relation.h
index 3a1255a..70d8fe7 100644
--- src/include/nodes/relation.h
+++ src/include/nodes/relation.h
@@ -1047,6 +1047,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 src/include/optimizer/pathnode.h src/include/optimizer/pathnode.h
index 71d9154..17094f9 100644
--- src/include/optimizer/pathnode.h
+++ src/include/optimizer/pathnode.h
@@ -149,6 +149,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 src/include/parser/kwlist.h src/include/parser/kwlist.h
index 581ff6e..78b2275 100644
--- src/include/parser/kwlist.h
+++ 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)
@@ -257,6 +258,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 src/include/parser/parse_node.h src/include/parser/parse_node.h
index bd6dc02..a3be05e 100644
--- src/include/parser/parse_node.h
+++ src/include/parser/parse_node.h
@@ -160,6 +160,12 @@ struct ParseState
 	RangeTblEntry *p_target_rangetblentry;
 
 	/*
+	 * 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 src/include/parser/parse_temporal.h src/include/parser/parse_temporal.h
new file mode 100644
index 0000000..235831e
--- /dev/null
+++ 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 src/include/utils/builtins.h src/include/utils/builtins.h
index 7ed1623..743345c 100644
--- src/include/utils/builtins.h
+++ src/include/utils/builtins.h
@@ -1245,6 +1245,7 @@ extern Datum uuid_hash(PG_FUNCTION_ARGS);
 
 /* windowfuncs.c */
 extern Datum window_row_number(PG_FUNCTION_ARGS);
+extern Datum window_row_id(PG_FUNCTION_ARGS);
 extern Datum window_rank(PG_FUNCTION_ARGS);
 extern Datum window_dense_rank(PG_FUNCTION_ARGS);
 extern Datum window_percent_rank(PG_FUNCTION_ARGS);
#10Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Peter Moser (#9)
Re: [PROPOSAL] Temporal query processing with range types

So I'm looking at this patch in the commit fest. I have only a general
understanding of temporal query processing.

What this patch does is to add two new clauses for FROM-list items,
NORMALIZE and ALIGN, which reshuffle a set of ranges into a new list
that can then be aggregated more easily. From the original message:

For NORMALIZE the tuples' ranges need to be split into all sub-ranges
according to all matching ranges of the second relation. For this we
create a subquery that first joins one relation with the range
boundaries of the other and then sorts the result. The executor
function splits the ranges in a sweep-line based manner.

For ALIGN the tuples' ranges must be split into all intersections and
differences with the other relation according to the join condition.
For this we create a subquery that first joins the two relations and
then sorts the result. The executor function splits the ranges
accordingly in a sweep-line based manner.

So there isn't really temporal query processing as such here, only some
helpers that can make it easier.

I can see how those operations can be useful, but it would help if there
were a more formal definition to be able to check that further.

What I'm missing here is some references: existing implementations,
standards, documentation, research papers, alternative ideas, rejected
alternatives, etc.

Also, the submission is missing documentation and test cases. There are
technical terms used in the code that I don't understand.

I think there are probably many interesting applications for normalizing
or otherwise adjusting ranges. I'd like to see an overview and
consideration of other applications.

Ideally, I'd like to see these things implemented as some kind of
user-space construct, like an operator or function. I think we'd need a
clearer definition of what it is they do before we can evaluate that.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#11Peter Moser
pitiz29a@gmail.com
In reply to: Peter Eisentraut (#10)
2 attachment(s)
Re: [PROPOSAL] Temporal query processing with range types

What this patch does is to add two new clauses for FROM-list items,
NORMALIZE and ALIGN, which reshuffle a set of ranges into a new list
that can then be aggregated more easily. From the original message:

For NORMALIZE the tuples' ranges need to be split into all sub-ranges
according to all matching ranges of the second relation. For this we
create a subquery that first joins one relation with the range
boundaries of the other and then sorts the result. The executor
function splits the ranges in a sweep-line based manner.

For ALIGN the tuples' ranges must be split into all intersections and
differences with the other relation according to the join condition.
For this we create a subquery that first joins the two relations and
then sorts the result. The executor function splits the ranges
accordingly in a sweep-line based manner.

So there isn't really temporal query processing as such here, only some
helpers that can make it easier.

The goal of temporal aligners and normalizers is to split ranges to allow a
reduction from temporal queries to their non-temporal counterparts.
Splitting
ranges is necessary for temporal query processing. Temporal aligners and
normalizer may then be used as building-blocks for any temporal query
construct.

I can see how those operations can be useful, but it would help if there
were a more formal definition to be able to check that further.

We have published two papers, that contain formal definitions and related
work
for the temporal aligner and normalizer. Please see [1]Anton Dignös, Michael H. Böhlen, Johann Gamper: Temporal alignment. SIGMOD Conference 2012: 433-444 http://doi.acm.org/10.1145/2213836.2213886 and [2]Anton Dignös, Michael H. Böhlen, Johann Gamper, Christian S. Jensen: Extending the Kernel of a Relational DBMS with Comprehensive Support for Sequenced Temporal Queries. ACM Trans. Database Syst. 41(4): 26:1-26:46 (2016) http://doi.acm.org/10.1145/2967608.

What I'm missing here is some references: existing implementations,
standards, documentation, research papers, alternative ideas, rejected
alternatives, etc.

A good overview of existing implementations in DBMSs, SQL standard, and
history
is given in [3]https://www2.cs.arizona.edu/people/rts/sql3.html and https://www2.cs.arizona.edu/people/rts/tsql2.html.

Also, the submission is missing documentation and test cases. There are
technical terms used in the code that I don't understand.

We added a second patch with test cases and expected results. We are now
writing the documentation in sgml-format.

I think there are probably many interesting applications for normalizing
or otherwise adjusting ranges. I'd like to see an overview and
consideration of other applications.

Please see the attached file adjustment.sql for some interesting
applications.

Ideally, I'd like to see these things implemented as some kind of
user-space construct, like an operator or function. I think we'd need a
clearer definition of what it is they do before we can evaluate that.

Can you please explain what you mean by "user-space construct" in this case.

Best regards,
Anton, Johann, Michael, Peter

----
[1]: Anton Dignös, Michael H. Böhlen, Johann Gamper: Temporal alignment. SIGMOD Conference 2012: 433-444 http://doi.acm.org/10.1145/2213836.2213886
Temporal alignment. SIGMOD Conference 2012: 433-444
http://doi.acm.org/10.1145/2213836.2213886
[2]: Anton Dignös, Michael H. Böhlen, Johann Gamper, Christian S. Jensen: Extending the Kernel of a Relational DBMS with Comprehensive Support for Sequenced Temporal Queries. ACM Trans. Database Syst. 41(4): 26:1-26:46 (2016) http://doi.acm.org/10.1145/2967608
Extending the Kernel of a Relational DBMS with Comprehensive Support for
Sequenced Temporal Queries. ACM Trans. Database Syst. 41(4): 26:1-26:46
(2016)
http://doi.acm.org/10.1145/2967608
[3]: https://www2.cs.arizona.edu/people/rts/sql3.html and https://www2.cs.arizona.edu/people/rts/tsql2.html
https://www2.cs.arizona.edu/people/rts/tsql2.html

Attachments:

adjustment.sqlapplication/sql; name=adjustment.sqlDownload
drop table emp1;
create table emp1(emp varchar(3), dept varchar(2), t int4range);
insert into emp1 values ('Sam', 'DB',  '[1,6)');
insert into emp1 values ('Ann', 'DB',  '[3,8)');
insert into emp1 values ('Ann', 'AI',  '[9,15)');
insert into emp1 values ('Joe', 'DB', '[14,19)');
select * from emp1;

drop table emp2;
create table emp2(emp varchar(3), dept varchar(2), t int4range);
insert into emp2 values ('Sam', 'DB',  '[4,11)');
insert into emp2 values ('Joe', 'DB', '[12,21)');
select * from emp2;

drop table proj;
create table proj(
  proj varchar(2), dept varchar(2), budg integer, t int4range);
insert into proj values ('P1', 'DB', 100,  '[2,7)');
insert into proj values ('P2', 'DB', 580, '[12,18)');
select * from proj;


-- ***********************************************************************
--  Example 1: temporal difference

--  Compute the temporal difference between emp1 and emp2, i.e., the
--  difference at each point in time.
--
--  Solution: We first normalize emp1 wrt emp2. Then we normalize emp2
--  wrt emp1. Then we compute the difference between the two
--  normalized relations.
--
-- ***********************************************************************

SELECT * FROM (emp1 NORMALIZE emp2 USING (emp, dept) WITH (t,t)) emp1a
EXCEPT
SELECT * FROM (emp2 NORMALIZE emp1 USING (emp, dept) WITH (t,t)) emp2a;

--  emp | dept |   t    
-- -----+------+--------
--  Ann | AI   | [9,15)
--  Sam | DB   | [1,4)
--  Ann | DB   | [3,8)


-- ***********************************************************************
--  Example 2: temporal aggregation

--  Compute the count of employees at each point in time.
--
--  Solution: We normalize emp wrt itself.  This splits ranges so that
--  all pair of ranges are either disjoint or identical. Standard
--  aggregation and grouping over the relation with the normalized
--  ranges yields the desired result.
--
-- ***********************************************************************

SELECT COUNT(*), t
FROM (emp1 NORMALIZE emp1 e2 USING () WITH (t,t)) emp1a
GROUP BY t
ORDER BY t;

--  count |    t    
-- -------+---------
--      1 | [1,3)
--      2 | [3,6)
--      1 | [6,8)
--      1 | [9,14)
--      2 | [14,15)
--      1 | [15,19)


-- ***********************************************************************
--  Example 3: temporal aggregation

--  For each department compute the count of employees at each point
--  in time. 
--
--  Solution: Similar to Example 2 except that we normalize the ranges
--  per department and also use the department as a grouping attribute
--  in the aggregation.
--
-- ***********************************************************************

SELECT dept, COUNT(*), t 
FROM (emp1 NORMALIZE emp1 e2 ON emp1.dept = e2.dept  WITH (t,t)) r
GROUP BY dept, t;

--  dept | count |    t    
-- ------+-------+---------
--  DB   |     2 | [3,6)
--  DB   |     1 | [14,19)
--  DB   |     1 | [6,8)
--  AI   |     1 | [9,15)
--  DB   |     1 | [1,3)

-- ***********************************************************************
--  Example 4: temporal antijoin
-- 
--  Determine the person with the largest budget. Determine the person
--  with the largest budget at each point in time.
--
--  Solution: For the first query a regular antijoin is used. It
--  yields the largest budget independent of the time. For the second
--  query we use the exact same approach except that the relations
--  must be aligned before computing the antijoin.
--
-- ***********************************************************************

drop table budg;
create table budg(name varchar(5), amnt integer, t daterange);
insert into budg values ('Joe', 5, '[2012/2/1,2012/9/1)');
insert into budg values ('Ann', 7, '[2012/5/1,2012/9/1)');
insert into budg values ('Per', 3, '[2012/4/1,2012/10/1)');
SELECT * FROM budg AS r;

SELECT *
FROM budg AS r
WHERE NOT EXISTS (
  SELECT *
  FROM budg AS s
  WHERE s.amnt > r.amnt );

--  name | amnt |            t            
-- ------+------+-------------------------
--  Ann  |    7 | [2012-05-01,2012-09-01)

SELECT *
FROM ( budg r ALIGN budg s ON s.amnt > r.amnt WITH (t,t)) r
WHERE NOT EXISTS (
  SELECT *
  FROM (budg s ALIGN budg r ON s.amnt > r.amnt WITH (t,t)) s
  WHERE s.amnt > r.amnt
  AND r.t = s.t  );

--  name | amnt |            t            
-- ------+------+-------------------------
--  Joe  |    5 | [2012-02-01,2012-05-01)
--  Ann  |    7 | [2012-05-01,2012-09-01)
--  Per  |    3 | [2012-09-01,2012-10-01)


-- ***********************************************************************
--  Example 5: average length of reservations at each point in time

--  Assume table res with hotel room reservation. For each point in time
--  determine the average duration of reservations.
--
--  Solution: We first retain and rename a copy of the range types that is
--  not affected by NORMALIZE, and then construct a temporal aggregation (as
--  as in Example 2) with the average duration over the copy of the range types.
--
-- ***********************************************************************

create table res(name varchar(5), t daterange);
insert into res values ('Ann', '[2012/1/1,2012/8/1)');
insert into res values ('Joe', '[2012/2/1,2012/7/1)');
insert into res values ('Ann', '[2012/8/1,2012/12/1)');

WITH c AS (SELECT t u, * FROM res)
SELECT cast(avg(upper(u) - lower(u)) as int), t
FROM ( c NORMALIZE res USING() with (t,t)) Radj
GROUP BY t;

--  avg |            t            
-- -----+-------------------------
--  213 | [2012-01-01,2012-02-01)
--  182 | [2012-02-01,2012-07-01)
--  213 | [2012-07-01,2012-08-01)
--  122 | [2012-08-01,2012-12-01)


-- ***********************************************************************
--  Example 6: temporal natural FOJ

--  Compute a temporal full outer natural join between emp1 and proj.
--  Include in the result the old and new interval timestamps.
--
--  Solution: We first retain and rename a copy of the range types that is
--  not affected by ALIGN, then we align emp1 wrt proj and proj
--  wrt emp1. Then we compute a NATURAL FULL JOIN between the two
--  aligned relations. 
--
--  Note: By replacing NATURAL FULL JOIN with NATURAL LEFT JOIN, NATURAL RIGHT 
--  JOIN or NATURAL JOIN the respective join type is performed.
--
-- ***********************************************************************

WITH
  emp1P AS ( SELECT emp, dept, t t1, t FROM emp1 ),
  projP AS ( SELECT proj, dept, t t2, t FROM proj )
SELECT r.emp, r.dept, r.t1, s.proj, s.dept, s.t2, t
FROM ( emp1P ALIGN proj ON emp1P.dept=proj.dept with (t,t)) r
     NATURAL FULL JOIN
     ( projP ALIGN emp1 ON emp1.dept=projP.dept with (t,t)) s
WHERE t = t1 * t2 OR t1 IS NULL OR t2 IS NULL;

--  emp | dept |   t1    | proj | dept |   t2    |    t    
-- -----+------+---------+------+------+---------+---------
--  Ann | AI   | [9,15)  |      |      |         | [9,15)
--  Sam | DB   | [1,6)   |      |      |         | [1,2)
--  Sam | DB   | [1,6)   | P1   | DB   | [2,7)   | [2,6)
--  Ann | DB   | [3,8)   | P1   | DB   | [2,7)   | [3,7)
--  Ann | DB   | [3,8)   |      |      |         | [7,8)
--      |      |         | P2   | DB   | [12,18) | [12,14)
--  Joe | DB   | [14,19) | P2   | DB   | [12,18) | [14,18)
--  Joe | DB   | [14,19) |      |      |         | [18,19)
tpg_primitives_out_tests_v1.patchtext/x-patch; charset=US-ASCII; name=tpg_primitives_out_tests_v1.patchDownload
diff --git src/test/regress/expected/temporal_primitives.out src/test/regress/expected/temporal_primitives.out
new file mode 100644
index 0000000..6e4cc0d
--- /dev/null
+++ src/test/regress/expected/temporal_primitives.out
@@ -0,0 +1,841 @@
+--
+-- TEMPORAL PRIMITIVES
+--
+SET datestyle TO ymd;
+CREATE COLLATION "de_DE.utf8" (LC_COLLATE = "de_DE.utf8",
+                               LC_CTYPE = "de_DE.utf8" );
+CREATE TEMP TABLE tpg_table1 (a char, b char, ts int, te int);
+CREATE TEMP TABLE tpg_table2 (c int, d char, ts int, te int);
+INSERT INTO tpg_table1 VALUES
+('a','B',1,7),
+('b','B',3,9),
+('c','G',8,10);
+INSERT INTO tpg_table2 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 tpg_table3 AS
+	SELECT a, ts, te, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table4 AS
+	SELECT c, ts, d, te FROM tpg_table2;
+-- VALID TIME columns represented as range type
+CREATE TEMP TABLE tpg_table5 AS
+	SELECT int4range(ts, te) t, a, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table6 AS
+	SELECT int4range(ts, te) t, c a, d b FROM tpg_table2;
+-- VALID TIME columns as VARCHARs
+CREATE TEMP TABLE tpg_table7 (a int, ts varchar, te varchar);
+CREATE TEMP TABLE tpg_table8 (a int,
+							  ts varchar COLLATE "de_DE.utf8",
+							  te varchar COLLATE "POSIX");
+INSERT INTO tpg_table7 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X'),
+(0, 'ABC', 'BCD'),
+(0, 'xABC', 'xBCD'),
+(0, 'BAA', 'BBB');
+INSERT INTO tpg_table8 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X');
+-- Tables to check different data types, and corner cases
+CREATE TEMP TABLE tpg_table9 (a int, ts timestamp, te timestamp);
+CREATE TEMP TABLE tpg_table10 (a int, ts double precision, te double precision);
+CREATE TEMP TABLE tpg_table11 AS TABLE tpg_table10;
+INSERT INTO tpg_table9 VALUES
+(0, '2000-01-01', '2000-01-10'),
+(1, '2000-01-05', '2000-01-20');
+INSERT INTO tpg_table10 VALUES
+(0, 1.0, 1.1111),
+(1, 1.11109999, 2.0);
+INSERT INTO tpg_table11 VALUES
+(0, 1.0, 'Infinity'),
+(1, '-Infinity', 2.0);
+--
+-- TEMPORAL ALIGNER: BASICS
+--
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 ALIGN tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | ts | te | 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) x;
+ a | ts | te | 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table1 ALIGN tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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)
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 ALIGN tpg_table6
+		ON TRUE
+		WITH (t, t)
+	) x;
+   t    | a | b 
+--------+---+---
+ [1,2)  | a | B
+ [2,5)  | a | B
+ [3,4)  | a | B
+ [5,7)  | a | B
+ [3,4)  | b | B
+ [3,5)  | b | B
+ [5,7)  | b | B
+ [7,9)  | b | B
+ [8,9)  | c | G
+ [9,10) | c | G
+(10 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 tpg_table5),
+	 t2 AS (SELECT *, t v FROM tpg_table6)
+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)
+
+-- Full temporal join example with absorbing where clause, timestamp
+-- propagation (see CTEs targetlists with V and U) and scalar VALID TIME columns
+WITH t1 AS (SELECT *, ts us, te ue FROM tpg_table1),
+	 t2 AS (SELECT *, ts vs, te ve FROM tpg_table2)
+SELECT x.ts, x.te, b, a, c FROM (
+	t1 ALIGN t2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	LEFT OUTER JOIN (
+		SELECT * FROM (
+		t2 ALIGN t1
+			ON b = d
+			WITH (ts, te, ts, te)
+		) y
+	) y
+	ON b = d AND x.ts = y.ts AND x.te = y.te
+	WHERE (
+			(x.ts = us OR x.ts = vs)
+			AND
+			(x.te = ue OR x.te = ve)
+		)
+		OR us IS NULL
+		OR vs IS NULL
+	ORDER BY 1,2,3,4;
+ ts | te | b | a | c 
+----+----+---+---+---
+  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 (
+	tpg_table7 x ALIGN tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ a |  ts  |  te  
+---+------+------
+ 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)
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x ALIGN tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ERROR:  could not determine which collation to use for string comparison
+HINT:  Use the COLLATE clause to set the collation explicitly.
+--
+-- 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 (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+                                                          QUERY PLAN                                                          
+------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(tpg_table2.ts, tpg_table1.ts)), (LEAST(tpg_table2.te, tpg_table1.te))
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table2.ts < tpg_table1.te) AND (tpg_table2.te > tpg_table1.ts))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Seq Scan on tpg_table1
+(11 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+                                                          QUERY PLAN                                                          
+------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: (x.ts > 3)
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(tpg_table2.ts, tpg_table1.ts)), (LEAST(tpg_table2.te, tpg_table1.te))
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table2.ts < tpg_table1.te) AND (tpg_table2.te > tpg_table1.ts))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Seq Scan on tpg_table1
+(12 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 3;
+                                                          QUERY PLAN                                                          
+------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: ((x.c < 3) OR (x.ts > 3))
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(tpg_table2.ts, tpg_table1.ts)), (LEAST(tpg_table2.te, tpg_table1.te))
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table2.ts < tpg_table1.te) AND (tpg_table2.te > tpg_table1.ts))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                     ->  Materialize
+                           ->  Seq Scan on tpg_table1
+(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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 ALIGN tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) 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, ts, te FROM (
+	tpg_table10 t1 ALIGN tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |     ts     |   te   
+---+------------+--------
+ 0 |          1 | 1.1111
+ 0 | 1.11109999 | 1.1111
+ 1 | 1.11109999 |      2
+(3 rows)
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 ALIGN tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |    ts     |    te    
+---+-----------+----------
+ 0 |         1 |        2
+ 0 |         1 | Infinity
+ 1 | -Infinity |        2
+(3 rows)
+
+--
+-- TEMPORAL NORMALIZER: BASICS
+--
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 NORMALIZE tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | ts | te | 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 ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT * FROM (
+	tpg_table3 NORMALIZE tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) x;
+ a | ts | te | 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 ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT a, COUNT(a) FROM (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table1 NORMALIZE tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 t1 NORMALIZE tpg_table1 t2
+		USING (a)
+		WITH (ts, te, ts, te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ a | B |  1 |  7
+ b | B |  3 |  9
+ c | G |  8 | 10
+(3 rows)
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 NORMALIZE tpg_table6
+		USING (b)
+		WITH (t, t)
+	) x;
+   t    | a | b 
+--------+---+---
+ [1,2)  | a | B
+ [2,3)  | a | B
+ [3,4)  | a | B
+ [4,5)  | a | B
+ [5,7)  | a | B
+ [3,4)  | b | B
+ [4,5)  | b | B
+ [5,7)  | b | B
+ [7,9)  | b | B
+ [8,10) | c | G
+(10 rows)
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	tpg_table7 x NORMALIZE tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ a |  ts  |  te  
+---+------+------
+ 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)
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x NORMALIZE tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ERROR:  could not determine which collation to use for string comparison
+HINT:  Use the COLLATE clause to set the collation explicitly.
+--
+-- 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 (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+                                               QUERY PLAN                                                
+---------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), tpg_table1.ts
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table1.ts >= tpg_table2.ts) AND (tpg_table1.ts < tpg_table2.te))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on tpg_table1
+                                 ->  Seq Scan on tpg_table1 tpg_table1_1
+(13 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+                                               QUERY PLAN                                                
+---------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: (x.ts > 3)
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), tpg_table1.ts
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table1.ts >= tpg_table2.ts) AND (tpg_table1.ts < tpg_table2.te))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on tpg_table1
+                                 ->  Seq Scan on tpg_table1 tpg_table1_1
+(14 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 3;
+                                               QUERY PLAN                                                
+---------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: ((x.c < 3) OR (x.ts > 3))
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), tpg_table1.ts
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table1.ts >= tpg_table2.ts) AND (tpg_table1.ts < tpg_table2.te))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on tpg_table1
+                                 ->  Seq Scan on tpg_table1 tpg_table1_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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 NORMALIZE tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) 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, ts, te FROM (
+	tpg_table10 t1 NORMALIZE tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |     ts     |     te     
+---+------------+------------
+ 0 |          1 | 1.11109999
+ 0 | 1.11109999 |     1.1111
+ 1 | 1.11109999 |          2
+(3 rows)
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 NORMALIZE tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |    ts     |    te    
+---+-----------+----------
+ 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 (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+TABLE v;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+TABLE v;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) NORMALIZE tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1", p1_1, ts, te)
+	) x;
+TABLE v;
+ p1 | p1_0 | p1_-1 | 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) ALIGN tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1",p1_1,ts,te)
+	) x;
+TABLE v;
+ p1 | p1_0 | p1_-1 | 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 src/test/regress/parallel_schedule src/test/regress/parallel_schedule
index 8641769..61813ef 100644
--- src/test/regress/parallel_schedule
+++ 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 tsrf
+test: alter_generic alter_operator misc psql async dbsize misc_functions tsrf temporal_primitives
 
 # rules cannot run concurrently with any test that creates a view
 test: rules psql_crosstab amutils
diff --git src/test/regress/sql/temporal_primitives.sql src/test/regress/sql/temporal_primitives.sql
new file mode 100644
index 0000000..9b475f7
--- /dev/null
+++ src/test/regress/sql/temporal_primitives.sql
@@ -0,0 +1,456 @@
+--
+-- TEMPORAL PRIMITIVES
+--
+SET datestyle TO ymd;
+
+CREATE COLLATION "de_DE.utf8" (LC_COLLATE = "de_DE.utf8",
+                               LC_CTYPE = "de_DE.utf8" );
+
+CREATE TEMP TABLE tpg_table1 (a char, b char, ts int, te int);
+CREATE TEMP TABLE tpg_table2 (c int, d char, ts int, te int);
+
+INSERT INTO tpg_table1 VALUES
+('a','B',1,7),
+('b','B',3,9),
+('c','G',8,10);
+INSERT INTO tpg_table2 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 tpg_table3 AS
+	SELECT a, ts, te, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table4 AS
+	SELECT c, ts, d, te FROM tpg_table2;
+
+-- VALID TIME columns represented as range type
+CREATE TEMP TABLE tpg_table5 AS
+	SELECT int4range(ts, te) t, a, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table6 AS
+	SELECT int4range(ts, te) t, c a, d b FROM tpg_table2;
+
+-- VALID TIME columns as VARCHARs
+CREATE TEMP TABLE tpg_table7 (a int, ts varchar, te varchar);
+CREATE TEMP TABLE tpg_table8 (a int,
+							  ts varchar COLLATE "de_DE.utf8",
+							  te varchar COLLATE "POSIX");
+
+INSERT INTO tpg_table7 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X'),
+(0, 'ABC', 'BCD'),
+(0, 'xABC', 'xBCD'),
+(0, 'BAA', 'BBB');
+
+INSERT INTO tpg_table8 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X');
+
+-- Tables to check different data types, and corner cases
+CREATE TEMP TABLE tpg_table9 (a int, ts timestamp, te timestamp);
+CREATE TEMP TABLE tpg_table10 (a int, ts double precision, te double precision);
+CREATE TEMP TABLE tpg_table11 AS TABLE tpg_table10;
+
+INSERT INTO tpg_table9 VALUES
+(0, '2000-01-01', '2000-01-10'),
+(1, '2000-01-05', '2000-01-20');
+
+INSERT INTO tpg_table10 VALUES
+(0, 1.0, 1.1111),
+(1, 1.11109999, 2.0);
+
+INSERT INTO tpg_table11 VALUES
+(0, 1.0, 'Infinity'),
+(1, '-Infinity', 2.0);
+
+
+--
+-- TEMPORAL ALIGNER: BASICS
+--
+
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+
+-- Alignment with aggregation
+-- NB: Targetlist of outer query is *not* A_STAR...
+SELECT a, COUNT(a) FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	GROUP BY a ORDER BY a;
+
+-- Test relations with differently named temporal bound attributes and relation
+-- and column aliases.
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 ALIGN tpg_table6
+		ON TRUE
+		WITH (t, t)
+	) 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 tpg_table5),
+	 t2 AS (SELECT *, t v FROM tpg_table6)
+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;
+
+-- Full temporal join example with absorbing where clause, timestamp
+-- propagation (see CTEs targetlists with V and U) and scalar VALID TIME columns
+WITH t1 AS (SELECT *, ts us, te ue FROM tpg_table1),
+	 t2 AS (SELECT *, ts vs, te ve FROM tpg_table2)
+SELECT x.ts, x.te, b, a, c FROM (
+	t1 ALIGN t2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	LEFT OUTER JOIN (
+		SELECT * FROM (
+		t2 ALIGN t1
+			ON b = d
+			WITH (ts, te, ts, te)
+		) y
+	) y
+	ON b = d AND x.ts = y.ts AND x.te = y.te
+	WHERE (
+			(x.ts = us OR x.ts = vs)
+			AND
+			(x.te = ue OR x.te = ve)
+		)
+		OR us IS NULL
+		OR vs IS NULL
+	ORDER BY 1,2,3,4;
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	tpg_table7 x ALIGN tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x ALIGN tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 ALIGN tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision
+SELECT a, ts, te FROM (
+	tpg_table10 t1 ALIGN tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 ALIGN tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+
+--
+-- TEMPORAL NORMALIZER: BASICS
+--
+
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+
+-- Alignment with aggregation
+-- NB: Targetlist of outer query is *not* A_STAR...
+SELECT a, COUNT(a) FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	GROUP BY a ORDER BY a;
+
+-- Test relations with differently named temporal bound attributes and relation
+-- and column aliases.
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+
+-- Normalizer's USING clause (self-normalization)
+SELECT * FROM (
+	tpg_table1 t1 NORMALIZE tpg_table1 t2
+		USING (a)
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 NORMALIZE tpg_table6
+		USING (b)
+		WITH (t, t)
+	) x;
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	tpg_table7 x NORMALIZE tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x NORMALIZE tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 NORMALIZE tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision
+SELECT a, ts, te FROM (
+	tpg_table10 t1 NORMALIZE tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 NORMALIZE tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+--
+-- TEMPORAL ALIGNER AND NORMALIZER: VIEWS
+--
+
+-- Views with temporal normalization
+CREATE TEMP VIEW v AS SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+
+TABLE v;
+DROP VIEW v;
+
+-- Views with temporal alignment
+CREATE TEMP VIEW v AS SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) NORMALIZE tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1", p1_1, ts, te)
+	) 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) ALIGN tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1",p1_1,ts,te)
+	) x;
+
+TABLE v;
+DROP VIEW v;
+
+
#12Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Peter Moser (#11)
Re: [PROPOSAL] Temporal query processing with range types

On 1/13/17 9:22 AM, Peter Moser wrote:

The goal of temporal aligners and normalizers is to split ranges to allow a
reduction from temporal queries to their non-temporal counterparts.
Splitting
ranges is necessary for temporal query processing. Temporal aligners and
normalizer may then be used as building-blocks for any temporal query
construct.

I would need to see the exact definitions of these constructs. Please
send some documentation.

We have published two papers, that contain formal definitions and
related work
for the temporal aligner and normalizer. Please see [1] and [2].

I don't have access to those.

I think there are probably many interesting applications for normalizing
or otherwise adjusting ranges. I'd like to see an overview and
consideration of other applications.

Please see the attached file adjustment.sql for some interesting
applications.

That's surely interesting, but without knowing what these operations are
supposed to do, I can only reverse engineer and guess.

Ideally, I'd like to see these things implemented as some kind of
user-space construct, like an operator or function. I think we'd need a
clearer definition of what it is they do before we can evaluate that.

Can you please explain what you mean by "user-space construct" in this case.

Implement them using the extensibility features, such as a user-defined
operator. I don't know if it's possible, but it's something to consider.

Using common terms such as ALIGN and NORMALIZE for such a specific
functionality seems a bit wrong.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#13Peter Moser
pitiz29a@gmail.com
In reply to: Peter Eisentraut (#12)
Re: [PROPOSAL] Temporal query processing with range types

2017-01-18 3:57 GMT+01:00 Peter Eisentraut <peter.eisentraut@2ndquadrant.com>:

On 1/13/17 9:22 AM, Peter Moser wrote:

The goal of temporal aligners and normalizers is to split ranges to allow a
reduction from temporal queries to their non-temporal counterparts.
Splitting
ranges is necessary for temporal query processing. Temporal aligners and
normalizer may then be used as building-blocks for any temporal query
construct.

I would need to see the exact definitions of these constructs. Please
send some documentation.

We have published two papers, that contain formal definitions and
related work
for the temporal aligner and normalizer. Please see [1] and [2].

I don't have access to those.

The papers can be freely downloaded from
http://www.inf.unibz.it/~dignoes/publications.html using the "Author-ize link".

I think there are probably many interesting applications for normalizing
or otherwise adjusting ranges. I'd like to see an overview and
consideration of other applications.

Please see the attached file adjustment.sql for some interesting
applications.

That's surely interesting, but without knowing what these operations are
supposed to do, I can only reverse engineer and guess.

Intuitively what they do is as follows:

NORMALIZE: splits all the ranges of one relation according to all the range
boundaries of another (but possibly the same) relation whenever some equality
condition over some given attributes is satisfied.

When the two relations are the same, all ranges with the given equal attributes
are either equal or disjoint. After this, the traditional GROUP BY or DISTINCT
can be applied. The attributes given to NORMALIZE for a (temporal) GROUP BY are
the grouping attributes and for a (temporal) DISTINCT the target list
attributes.

When the two relations are different, but they each contain disjoint ranges
for the same attributes (as the current limitation for the set operations is)
we perform a symmetric NORMALIZE on each of them. Then we have a similar
situation as before, i.e., in both relations ranges with the same attributes
are either equal or disjoint and a traditional set operation
(EXCEPT/INTERSECT/UNION) can be applied. The attributes given to NORMALIZE for
a (temporal) EXCEPT/INTERSECT/UNION are the target list attributes.

ALIGN: splits all the ranges of one relation according to all the range
intersections of another relation, i.e., it produces all intersections and
non-overlapping parts, whenever some condition is satisfied.

We perform a symmetric ALIGN on each relation, after which a traditional inner
or outer join can be applied using equality on the ranges to calculate the
overlap. The condition given to a (temporal) inner or outer join is the
join condition without overlap.

Ideally, I'd like to see these things implemented as some kind of
user-space construct, like an operator or function. I think we'd need a
clearer definition of what it is they do before we can evaluate that.

Can you please explain what you mean by "user-space construct" in this case.

Implement them using the extensibility features, such as a user-defined
operator. I don't know if it's possible, but it's something to consider.

We experimented with user-defined operators and set-returning functions. The
main issue with these is that ALIGN and NORMALIZE rely on comparison and
processing of one tuple with many tuples at a time that is not possible with
operators, and for set-returning functions there is no clean way of passing
tables and/or conditions.

Using common terms such as ALIGN and NORMALIZE for such a specific
functionality seems a bit wrong.

Would ALIGN RANGES/RANGE ALIGN and NORMALIZE RANGES/RANGE NORMALIZE be better
options? We are also thankful for any suggestion or comments about the syntax.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#14Michael Paquier
michael.paquier@gmail.com
In reply to: Peter Moser (#13)
Re: [PROPOSAL] Temporal query processing with range types

On Tue, Jan 24, 2017 at 6:32 PM, Peter Moser <pitiz29a@gmail.com> wrote:

[reviews and discussions]

The patch proposed has rotten. Please provide a rebase. By the way, I
am having a hard time applying your patches with patch or any other
methods... I am moving it to CF 2017-03 because of the lack of
reviews.
--
Michael

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#15Peter Moser
pitiz29a@gmail.com
In reply to: Michael Paquier (#14)
2 attachment(s)
Re: [PROPOSAL] Temporal query processing with range types

2017-02-01 6:19 GMT+01:00 Michael Paquier <michael.paquier@gmail.com>:

The patch proposed has rotten. Please provide a rebase. By the way, I
am having a hard time applying your patches with patch or any other
methods... I am moving it to CF 2017-03 because of the lack of
reviews.

We have rebased our patches on top of commit
53dd2da257fb5904b087b97dd9c2867390d309c1
from "Thu Feb 2 14:12:35 2017 +0200".

Hereby, we used the following commands to create both patches:
git diff --no-prefix origin/master -- src/ ':!src/test/*' >
tpg_primitives_out_v4.patch

git diff --no-prefix origin/master -- src/test/ >
tpg_primitives_out_tests_v2.patch

We have also tested our patches on the current HEAD with the command:
patch -p0 < patch-file

Both worked without problems or warnings on our Linux machine.
Could you please explain, which problems occurred while you tried to
apply our patches?

Best regards,
Anton, Johann, Michael, Peter

Attachments:

tpg_primitives_out_v4.patchtext/x-patch; charset=US-ASCII; name=tpg_primitives_out_v4.patchDownload
diff --git src/backend/commands/explain.c src/backend/commands/explain.c
index 0a67be0..04ed3bf 100644
--- src/backend/commands/explain.c
+++ src/backend/commands/explain.c
@@ -899,6 +899,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 src/backend/executor/Makefile src/backend/executor/Makefile
index 2a2b7eb..490a1c6 100644
--- src/backend/executor/Makefile
+++ src/backend/executor/Makefile
@@ -26,6 +26,8 @@ OBJS = execAmi.o execCurrent.o execGrouping.o execIndexing.o execJunk.o \
        nodeSamplescan.o nodeSeqscan.o nodeSetOp.o nodeSort.o nodeUnique.o \
        nodeValuesscan.o nodeCtescan.o nodeWorktablescan.o \
        nodeGroup.o nodeSubplan.o nodeSubqueryscan.o nodeTidscan.o \
-       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o
+       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o \
+       nodeTemporalAdjustment.o
+
 
 include $(top_srcdir)/src/backend/common.mk
diff --git src/backend/executor/execProcnode.c src/backend/executor/execProcnode.c
index 0dd95c6..9fe738c 100644
--- src/backend/executor/execProcnode.c
+++ src/backend/executor/execProcnode.c
@@ -115,6 +115,7 @@
 #include "executor/nodeValuesscan.h"
 #include "executor/nodeWindowAgg.h"
 #include "executor/nodeWorktablescan.h"
+#include "executor/nodeTemporalAdjustment.h"
 #include "nodes/nodeFuncs.h"
 #include "miscadmin.h"
 
@@ -340,6 +341,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 */
@@ -541,6 +547,10 @@ ExecProcNode(PlanState *node)
 			result = ExecLimit((LimitState *) node);
 			break;
 
+		case T_TemporalAdjustmentState:
+			result = ExecTemporalAdjustment((TemporalAdjustmentState *) node);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
 			result = NULL;
@@ -793,6 +803,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;
@@ -826,3 +840,4 @@ ExecShutdownNode(PlanState *node)
 
 	return planstate_tree_walker(node, ExecShutdownNode, NULL);
 }
+
diff --git src/backend/executor/nodeTemporalAdjustment.c src/backend/executor/nodeTemporalAdjustment.c
new file mode 100644
index 0000000..2afe0ec
--- /dev/null
+++ src/backend/executor/nodeTemporalAdjustment.c
@@ -0,0 +1,536 @@
+#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 */
+
+/*
+ * #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);
+}
+
+/*
+ * 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->attNumTs - 1] = newTs;
+	node->newValues[node->temporalCl->attNumTe - 1] = newTe;
+	if(node->temporalCl->attNumTr != -1)
+		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.ts, r.te, s.ts, s.te)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1, P2)
+ * 	  where r_1,...,r_n are all attributes from relation r. Two of these
+ * 	  attributes are temporal boundaries, namely TS and TE. The interval
+ * 	  [TS,TE) 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.ts, r.te, s.ts, s.te)) x
+ * 	  or
+ * 	  ... FROM (r NORMALIZE s ON theta WITH (r.ts, r.te, s.ts, s.te)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1)
+ * 	  where r_1,...,r_n are all attributes from relation r. Two of these
+ * 	  attributes are temporal boundaries, namely TS and TE. The interval
+ * 	  [TS,TE) represents the VALID TIME of each tuple. RN is the
+ * 	  temporal group ID or row number, and P1 is union of both
+ * 	  timepoints TS and TE 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;
+	Datum 				 currP2;
+	Datum				 currRN;
+	Datum				 prevRN;
+	Datum				 prevTe;
+
+	if(node->firstCall)
+	{
+		curr = ExecProcNode(outerPlan);
+		if(TupIsNull(curr))
+			return NULL;
+
+		prev = ExecCopySlot(prev, curr);
+		node->sameleft = true;
+		node->firstCall = false;
+		node->outrn = 0;
+		node->datumFormat = curr->tts_tupleDescriptor->attrs[tc->attNumTs - 1];
+		setSweepline(slotGetAttrNotNull(curr, tc->attNumTs));
+	}
+
+	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(heapGetAttrNotNull(out, tc->attNumTs),
+									currP1,
+									node)
+						|| !isEqual(heapGetAttrNotNull(out, tc->attNumTe),
+									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 = heapGetAttrNotNull(prev, tc->attNumTe);
+
+			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(slotGetAttrNotNull(curr, tc->attNumTs));
+			}
+			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));
+
+	/* 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;
+
+	/*
+	 * Miscellaneous initialization
+	 *
+	 * Unique 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.
+	 */
+	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 align 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->attNumTs - 1] = true;
+	state->tsteMask[state->temporalCl->attNumTe - 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);
+
+	/*
+	 * Range types in boundaries need special treatment:
+	 * - there is an extra column in each tuple that must be changed
+	 * - and a range constructor method that must be called
+	 */
+	if(node->temporalCl->attNumTr != -1)
+	{
+		state->tsteMask[state->temporalCl->attNumTr - 1] = true;
+		fmgr_info(fmgr_internal_function("range_constructor2"), rcFunctionInfo);
+		rcFunctionInfo->fn_expr = (fmNodePtr) node->rangeVar;
+		InitFunctionCallInfoData(state->rcFuncCallInfo, rcFunctionInfo, 2,
+								 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));
+}
+
+
+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 src/backend/nodes/copyfuncs.c src/backend/nodes/copyfuncs.c
index 30d733e..2dc3be1 100644
--- src/backend/nodes/copyfuncs.c
+++ src/backend/nodes/copyfuncs.c
@@ -1967,6 +1967,9 @@ _copyJoinExpr(const JoinExpr *from)
 	COPY_NODE_FIELD(quals);
 	COPY_NODE_FIELD(alias);
 	COPY_SCALAR_FIELD(rtindex);
+	COPY_NODE_FIELD(temporalBounds);
+	COPY_SCALAR_FIELD(inTmpPrimTempType);
+	COPY_SCALAR_FIELD(inTmpPrimHasRangeT);
 
 	return newnode;
 }
@@ -2328,6 +2331,45 @@ _copyOnConflictClause(const OnConflictClause *from)
 	return newnode;
 }
 
+static TemporalClause *
+_copyTemporalClause(const TemporalClause *from)
+{
+	TemporalClause *newnode = makeNode(TemporalClause);
+
+	COPY_SCALAR_FIELD(temporalType);
+	COPY_SCALAR_FIELD(attNumTs);
+	COPY_SCALAR_FIELD(attNumTe);
+	COPY_SCALAR_FIELD(attNumTr);
+	COPY_SCALAR_FIELD(attNumP1);
+	COPY_SCALAR_FIELD(attNumP2);
+	COPY_SCALAR_FIELD(attNumRN);
+	COPY_STRING_FIELD(colnameTs);
+	COPY_STRING_FIELD(colnameTe);
+	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)
 {
@@ -2787,6 +2829,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);
 
@@ -2868,6 +2911,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);
@@ -5284,6 +5328,12 @@ copyObject(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 src/backend/nodes/equalfuncs.c src/backend/nodes/equalfuncs.c
index 55c73b7..97f98a5 100644
--- src/backend/nodes/equalfuncs.c
+++ src/backend/nodes/equalfuncs.c
@@ -755,6 +755,9 @@ _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);
+	COMPARE_SCALAR_FIELD(inTmpPrimHasRangeT);
 
 	return true;
 }
diff --git src/backend/nodes/makefuncs.c src/backend/nodes/makefuncs.c
index 7586cce..0a2297f 100644
--- src/backend/nodes/makefuncs.c
+++ 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 -
@@ -610,3 +693,4 @@ makeGroupingSet(GroupingSetKind kind, List *content, int location)
 	n->location = location;
 	return n;
 }
+
diff --git src/backend/nodes/outfuncs.c src/backend/nodes/outfuncs.c
index 1560ac3..cf1c48c 100644
--- src/backend/nodes/outfuncs.c
+++ src/backend/nodes/outfuncs.c
@@ -1558,6 +1558,9 @@ _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);
+	WRITE_BOOL_FIELD(inTmpPrimHasRangeT);
 }
 
 static void
@@ -1836,6 +1839,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");
@@ -2524,6 +2539,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);
@@ -2600,6 +2616,38 @@ _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(attNumTs);
+	WRITE_INT_FIELD(attNumTe);
+	WRITE_INT_FIELD(attNumTr);
+	WRITE_INT_FIELD(attNumP1);
+	WRITE_INT_FIELD(attNumP2);
+	WRITE_INT_FIELD(attNumRN);
+	WRITE_STRING_FIELD(colnameTs);
+	WRITE_STRING_FIELD(colnameTe);
+	WRITE_STRING_FIELD(colnameTr);
+}
+
+static void
 _outColumnDef(StringInfo str, const ColumnDef *node)
 {
 	WRITE_NODE_TYPE("COLUMNDEF");
@@ -2731,6 +2779,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);
@@ -3706,6 +3755,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;
@@ -3803,6 +3855,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);
diff --git src/backend/nodes/print.c src/backend/nodes/print.c
index 926f226..a62463b 100644
--- src/backend/nodes/print.c
+++ src/backend/nodes/print.c
@@ -25,7 +25,7 @@
 #include "optimizer/clauses.h"
 #include "parser/parsetree.h"
 #include "utils/lsyscache.h"
-
+#include "parser/parse_node.h"
 
 /*
  * print
@@ -492,3 +492,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 src/backend/nodes/readfuncs.c src/backend/nodes/readfuncs.c
index dcfa6ee..65f26f1 100644
--- src/backend/nodes/readfuncs.c
+++ 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,28 @@ _readSetOperationStmt(void)
 	READ_DONE();
 }
 
+/*
+ * _readTemporalClause
+ */
+static TemporalClause *
+_readTemporalClause(void)
+{
+	READ_LOCALS(TemporalClause);
+
+	READ_ENUM_FIELD(temporalType, TemporalType);
+	READ_INT_FIELD(attNumTs);
+	READ_INT_FIELD(attNumTe);
+	READ_INT_FIELD(attNumTr);
+	READ_INT_FIELD(attNumP1);
+	READ_INT_FIELD(attNumP2);
+	READ_INT_FIELD(attNumRN);
+	READ_STRING_FIELD(colnameTs);
+	READ_STRING_FIELD(colnameTe);
+	READ_STRING_FIELD(colnameTr);
+
+	READ_DONE();
+}
+
 
 /*
  *	Stuff from primnodes.h.
@@ -460,6 +483,17 @@ _readRangeVar(void)
 	READ_DONE();
 }
 
+static ColumnRef *
+_readColumnRef(void)
+{
+	READ_LOCALS(ColumnRef);
+
+	READ_NODE_FIELD(fields);
+	READ_LOCATION_FIELD(location);
+
+	READ_DONE();
+}
+
 static IntoClause *
 _readIntoClause(void)
 {
@@ -1241,6 +1275,9 @@ _readJoinExpr(void)
 	READ_NODE_FIELD(quals);
 	READ_NODE_FIELD(alias);
 	READ_INT_FIELD(rtindex);
+	READ_NODE_FIELD(temporalBounds);
+	READ_ENUM_FIELD(inTmpPrimTempType, TemporalType);
+	READ_BOOL_FIELD(inTmpPrimHasRangeT);
 
 	READ_DONE();
 }
@@ -2457,6 +2494,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 src/backend/optimizer/path/allpaths.c src/backend/optimizer/path/allpaths.c
index 5c18987..f1569f0 100644
--- src/backend/optimizer/path/allpaths.c
+++ 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 */
@@ -126,6 +127,7 @@ static void subquery_push_qual(Query *subquery,
 static void recurse_push_qual(Node *setOp, Query *topquery,
 				  RangeTblEntry *rte, Index rti, Node *qual);
 static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
+static bool allWindowFuncsHaveRowId(List *targetList);
 static int	compute_parallel_worker(RelOptInfo *rel, BlockNumber pages);
 
 
@@ -901,11 +903,11 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
 			Assert(IsA(rinfo, RestrictInfo));
 			childqual = adjust_appendrel_attrs(root,
 											   (Node *) rinfo->clause,
-											   appinfo);
+													 appinfo);
 			childqual = eval_const_expressions(root, childqual);
 			/* check for flat-out constant */
 			if (childqual && IsA(childqual, Const))
-			{
+		{
 				if (((Const *) childqual)->constisnull ||
 					!DatumGetBool(((Const *) childqual)->constvalue))
 				{
@@ -944,7 +946,7 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
 			}
 		}
 
-		/*
+			/*
 		 * In addition to the quals inherited from the parent, we might have
 		 * securityQuals associated with this particular child node.
 		 * (Currently this can only happen in appendrels originating from
@@ -2355,7 +2357,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;
 
 	/*
@@ -2465,6 +2468,7 @@ static void
 check_output_expressions(Query *subquery, pushdown_safety_info *safetyInfo)
 {
 	ListCell   *lc;
+	bool wfsafe = allWindowFuncsHaveRowId(subquery->targetList);
 
 	foreach(lc, subquery->targetList)
 	{
@@ -2503,12 +2507,35 @@ 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 resnoTs =
+					((TemporalClause *)subquery->temporalClause)->attNumTs;
+			AttrNumber resnoTe =
+					((TemporalClause *)subquery->temporalClause)->attNumTe;
+			AttrNumber resnoRangeT =
+					((TemporalClause *)subquery->temporalClause)->attNumTr;
+
+			if (tle->resno == resnoTs
+					|| tle->resno == resnoTe
+					|| tle->resno == resnoRangeT)
+			{
+				safetyInfo->unsafeColumns[tle->resno] = true;
+				continue;
+			}
+		}
 	}
 }
 
@@ -2578,6 +2605,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
@@ -2821,6 +2874,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 src/backend/optimizer/plan/createplan.c src/backend/optimizer/plan/createplan.c
index fae1f67..5e29a58 100644
--- src/backend/optimizer/plan/createplan.c
+++ 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,
@@ -272,6 +275,9 @@ static ModifyTable *make_modifytable(PlannerInfo *root,
 				 List *resultRelations, List *subplans,
 				 List *withCheckOptionLists, List *returningLists,
 				 List *rowMarks, OnConflictExpr *onconflict, int epqParam);
+static TemporalAdjustment *make_temporalAdjustment(Plan *lefttree,
+						TemporalClause *temporalClause,
+						List *sortClause);
 
 
 /*
@@ -469,6 +475,11 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
 											  (LimitPath *) best_path,
 											  flags);
 			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);
@@ -2277,6 +2288,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;
+}
+
 
 /*****************************************************************************
  *
@@ -4894,6 +4932,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 = 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 src/backend/optimizer/plan/planner.c src/backend/optimizer/plan/planner.c
index 4b5902f..f66e0ab 100644
--- src/backend/optimizer/plan/planner.c
+++ src/backend/optimizer/plan/planner.c
@@ -1986,6 +1986,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
@@ -4348,7 +4362,6 @@ create_ordered_paths(PlannerInfo *root,
 	return ordered_rel;
 }
 
-
 /*
  * make_group_input_target
  *	  Generate appropriate PathTarget for initial input to grouping nodes.
diff --git src/backend/optimizer/plan/setrefs.c src/backend/optimizer/plan/setrefs.c
index be267b9..bc93f33 100644
--- src/backend/optimizer/plan/setrefs.c
+++ src/backend/optimizer/plan/setrefs.c
@@ -612,6 +612,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 src/backend/optimizer/plan/subselect.c src/backend/optimizer/plan/subselect.c
index 9fc7489..b8ed9ec 100644
--- src/backend/optimizer/plan/subselect.c
+++ src/backend/optimizer/plan/subselect.c
@@ -2688,6 +2688,7 @@ finalize_plan(PlannerInfo *root, Plan *plan, Bitmapset *valid_params,
 		case T_Gather:
 		case T_SetOp:
 		case T_Group:
+		case T_TemporalAdjustment:
 			/* no node-type-specific fields need fixing */
 			break;
 
diff --git src/backend/optimizer/util/pathnode.c src/backend/optimizer/util/pathnode.c
index f440875..cab02ce 100644
--- src/backend/optimizer/util/pathnode.c
+++ src/backend/optimizer/util/pathnode.c
@@ -2428,6 +2428,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 src/backend/parser/Makefile src/backend/parser/Makefile
index fdd8485..35c20fa 100644
--- src/backend/parser/Makefile
+++ src/backend/parser/Makefile
@@ -15,7 +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_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_relation.o parse_target.o parse_type.o parse_utilcmd.o scansup.o \
+      parse_temporal.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git src/backend/parser/analyze.c src/backend/parser/analyze.c
index 0f7659b..60e4248 100644
--- src/backend/parser/analyze.c
+++ 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"
@@ -1214,6 +1215,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");
@@ -1291,6 +1295,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 src/backend/parser/gram.y src/backend/parser/gram.y
index a4edea0..8c4f8fb 100644
--- src/backend/parser/gram.y
+++ src/backend/parser/gram.y
@@ -425,6 +425,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
@@ -476,11 +477,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
@@ -574,6 +578,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>		partbound_datum_list
 %type <partrange_datum>	PartitionRangeDatum
 %type <list>		range_datum_list
+%type <list>    temporal_bounds_list
+
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -598,7 +604,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
@@ -644,7 +650,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
 
@@ -11084,6 +11090,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.
@@ -11340,6 +11359,94 @@ 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 period boundaries with 4 comparable values
+					 * or two range typed values,
+					 * i.e. [lts, lte) is the left argument period, and
+					 *		[rts, rte) is the right argument period,
+					 * or two compatible range types with bounds like '[)'
+					 */
+					if(list_length($6) == 4 || list_length($6) == 2)
+						n->temporalBounds = $6;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have 2 range typed values, or four " \
+										"single 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 period boundaries with 4 comparable values
+					 * or two range typed values,
+					 * i.e. [lts, lte) is the left argument period, and
+					 *      [rts, rte) is the right argument period,
+					 * or two compatible range types with bounds like '[)'
+					 */
+					if(list_length($5) == 4 || list_length($5) == 2)
+						n->temporalBounds = $5;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have 2 range typed values, or four " \
+										"single values."),
+								 parser_errposition(@5)));
+
+					$$ = n;
+				}
 		;
 
 
@@ -14653,7 +14760,8 @@ type_func_name_keyword:
  * forced to.
  */
 reserved_keyword:
-			  ALL
+			  ALIGN
+			| ALL
 			| ANALYSE
 			| ANALYZE
 			| AND
@@ -14701,6 +14809,7 @@ reserved_keyword:
 			| LIMIT
 			| LOCALTIME
 			| LOCALTIMESTAMP
+			| NORMALIZE
 			| NOT
 			| NULL_P
 			| OFFSET
diff --git src/backend/parser/parse_clause.c src/backend/parser/parse_clause.c
index 69f4736..f65ae6a 100644
--- src/backend/parser/parse_clause.c
+++ src/backend/parser/parse_clause.c
@@ -39,6 +39,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"
@@ -941,6 +942,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.
 		 */
@@ -1003,6 +1041,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 src/backend/parser/parse_temporal.c src/backend/parser/parse_temporal.c
new file mode 100644
index 0000000..3a3f86f
--- /dev/null
+++ src/backend/parser/parse_temporal.c
@@ -0,0 +1,1620 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 can have four elements in a boundary
+ * list (i.e., WITH-clause of a temporal primitive) when we have two argument
+ * relations with scalar boundaries, or two entries if we have 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_LARGTST = 0,
+	TPB_LARGTE,
+	TPB_RARGTST,
+	TPB_RARGTE
+} 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,
+						  bool *hasRangeTypes,
+						  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,
+						   bool hasRangeTypes,
+						   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. These are either
+ * 		two scalar values for TS and TE, or a single range type value T holding
+ * 		both bounds.
+ */
+static List *
+temporalBoundGetLeftBounds(List *bounds)
+{
+	switch(list_length(bounds))
+	{
+		case 2: return list_make1(linitial(bounds));
+		case 4: return list_make2(linitial(bounds), lsecond(bounds));
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT),
+			 errmsg("Invalid temporal bound list length."),
+			 errhint("Specify four scalar columns for the " \
+					 "temporal boundaries, or two range-typed "\
+					 "columns.")));
+
+	/* Keep compiler quiet */
+	return NIL;
+}
+
+/*
+ * temporalBoundGetRightBounds -
+ * 		Return the right boundaries of a temporal bounds list. These are either
+ * 		two scalar values for TS and TE, or a single range type value T holding
+ * 		both bounds.
+ */
+static List *
+temporalBoundGetRightBounds(List *bounds)
+{
+	switch(list_length(bounds))
+	{
+		case 2: return list_make1(lsecond(bounds));
+		case 4: return list_make2(lthird(bounds), lfourth(bounds));
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT),
+			 errmsg("Invalid temporal bound list length."),
+			 errhint("Specify four scalar columns for the " \
+					 "temporal boundaries, or two range-typed "\
+					 "columns.")));
+
+	/* Keep compiler quiet */
+	return NIL;
+}
+
+/*
+ * 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)
+	{
+		/*
+		 * Four boundary entries means that we have 2x two scalar boundaries.
+		 * Which means the first two entries are start and end of the first
+		 * bound, and the 3th and 4th entry are start and end of the second
+		 * bound.
+		 */
+		case 4:
+			return list_nth(bounds, id);
+
+		/*
+		 * 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_LARGTST)
+				return linitial(bounds);
+			if(id == TPB_RARGTST || id == TPB_LARGTE)
+				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_LARGTST)
+				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 four scalar columns for the " \
+						 "temporal boundaries, or two range-typed "\
+						 "columns.")));
+
+	return NULL;
+}
+
+/*
+ * transformTemporalClause -
+ * 		If we have a temporal primitive query, we must find all attribute
+ * 		numbers for p1, p2, rn, ts, te, 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, all attribute numbers for temporal boundaries are unknown */
+	tc->attNumTr = -1;
+	tc->attNumTe = -1;
+	tc->attNumTs = -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;
+	}
+
+	/*
+	 * If we have range types the subquery splits it into separate
+	 * columns, called ts and te which are in between the p1- and
+	 * rn-column.
+	 */
+	if(tc->colnameTr)
+	{
+		tc->attNumTe = pos--;
+		tc->attNumTs = pos--;
+	}
+
+	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 if(strcmp(key, "ts") == 0)
+		{
+			tc->colnameTs = pstrdup(value);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumTs);
+			foundTsTe = true;
+		}
+		else if(strcmp(key, "te") == 0)
+		{
+			tc->colnameTe = pstrdup(value);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumTe);
+			foundTsTe = true;
+		}
+		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;
+
+		/* Temporal boundary is a range type */
+		if (tc->colnameTr)
+		{
+			if (strcmp(tle->resname, tc->colnameTr) == 0)
+				tc->attNumTr = tle->resno;
+		}
+		else /* Two scalar columns for boundaries */
+		{
+			if (strcmp(tle->resname, tc->colnameTs) == 0)
+				tc->attNumTs = tle->resno;
+			else if (strcmp(tle->resname, tc->colnameTe) == 0)
+				tc->attNumTe = tle->resno;
+		}
+	}
+
+	/* We need column attribute numbers for all temporal boundaries */
+	if(tc->attNumTs == -1
+			|| tc->attNumTe == -1
+			|| (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;
+
+	/* We use range types, remove splitted columns, i.e. upper/lower bounds */
+	if(tc->colnameTr) {
+		get_tle_by_resno(qry->targetList, tc->attNumTs)->resjunk = true;
+		get_tle_by_resno(qry->targetList, tc->attNumTe)->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, bool *hasRangeTypes, 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;
+
+	/*
+	 * We can have 2 or 4 column references, i.e. if we have 4, the first two
+	 * form the left argument period [largTs, largTe), and the last two the
+	 * right argument period respectively. Otherwise, we have two range typed
+	 * values of the form '[)' where the first argument contains the boundaries
+	 * of the left-hand-side, and the second argument contains the boundaries
+	 * of the RHS respectively. The parser checked already if there was another
+	 * number of arguments (not equal to 2 or 4) given.
+	 */
+	*hasRangeTypes = list_length(j->temporalBounds) == 2;
+
+	/*
+	 * 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);
+	if(*hasRangeTypes)
+	{
+		/*
+		 * Hardcoded column names for ts and te. We handle ambiguous column
+		 * names during the transformation of temporal primitive clauses.
+		 */
+		ssResult->temporalClause->colnameTs = "ts";
+		ssResult->temporalClause->colnameTe = "te";
+		ssResult->temporalClause->colnameTr =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTST);
+	}
+	else
+	{
+		ssResult->temporalClause->colnameTs =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTST);
+		ssResult->temporalClause->colnameTe =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTE);
+		ssResult->temporalClause->colnameTr = NULL;
+	}
+
+	/*
+	 * 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;
+	joinExpr->inTmpPrimHasRangeT = *hasRangeTypes;
+
+	return ssResult;
+}
+
+/*
+ * transformTemporalAligner -
+ * 		transform a TEMPORAL ALIGN clause into standard SQL
+ *
+ * INPUT:
+ * 		(r ALIGN s ON q WITH (r.ts, r.te, s.ts, s.te)) c
+ * 		where q can be any join qualifier, and r.ts, r.te, s.ts, and s.te
+ * 		can be any column name.
+ *
+ * OUTPUT:
+ * 		(
+ * 		SELECT r.*, GREATEST(r.ts, s.ts) P1, LEAST(r.te, s.te) P2
+ *      FROM
+ *      (
+ *      	SELECT *, row_id() OVER () rn FROM r
+ *      ) r
+ *      LEFT OUTER JOIN
+ *      s
+ *      ON q AND r.ts < s.te AND r.te > s.ts
+ *      ORDER BY rn, P1, P2
+ *      ) c
+ */
+Node *
+transformTemporalAligner(ParseState *pstate, JoinExpr *j)
+{
+	const int 	 		 UNKNOWN_LOCATION = -1;
+
+	bool				 hasRangeTypes;
+	SelectStmt			*ssResult;
+	RangeSubselect 		*rssResult;
+	ResTarget			*rtGreatest;
+	ResTarget			*rtLeast;
+	ResTarget			*rtLowerLarg;
+	ResTarget			*rtUpperLarg;
+	ColumnRef 			*crLargTs;
+	ColumnRef 			*crRargTs;
+	ColumnRef 			*crLargTe;
+	ColumnRef 			*crRargTe;
+	MinMaxExpr			*mmeGreatest;
+	MinMaxExpr			*mmeLeast;
+	FuncCall			*fcLowerLarg;
+	FuncCall			*fcLowerRarg;
+	FuncCall			*fcUpperLarg;
+	FuncCall			*fcUpperRarg;
+	List				*mmeGreatestArgs;
+	List				*mmeLeastArgs;
+	List				*boundariesExpr;
+	JoinExpr			*joinExpr;
+	A_Expr				*lowerBoundExpr;
+	A_Expr				*upperBoundExpr;
+	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, &hasRangeTypes,
+										 &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.
+	 */
+	if (hasRangeTypes)
+	{
+		crLargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crRargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   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);
+
+		rtLowerLarg = makeResTarget((Node *) fcLowerLarg,
+									ssResult->temporalClause->colnameTs);
+		rtUpperLarg = makeResTarget((Node *) fcUpperLarg,
+									ssResult->temporalClause->colnameTe);
+
+		ssResult->targetList = list_concat(ssResult->targetList,
+										   list_make2(rtLowerLarg, rtUpperLarg));
+	}
+	else
+	{
+		crLargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crLargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTE,
+										   largAlias->aliasname);
+		crRargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+		crRargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTE,
+										   rargAlias->aliasname);
+
+		/* Create argument list for function call to "greatest" and "least" */
+		mmeGreatestArgs = list_make2(crLargTs, crRargTs);
+		mmeLeastArgs = list_make2(crLargTe, crRargTe);
+
+		/*
+		 * Build Boolean expressions, i.e. "r.ts < s.te AND r.te > s.ts"
+		 * and concatenate it with q (=theta)
+		 */
+		lowerBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  "<",
+										  copyObject(crLargTs),
+										  copyObject(crRargTe),
+										  UNKNOWN_LOCATION);
+		upperBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  ">",
+										  copyObject(crLargTe),
+										  copyObject(crRargTs),
+										  UNKNOWN_LOCATION);
+
+		boundariesExpr = list_make2(lowerBoundExpr, upperBoundExpr);
+	}
+
+	/* 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.ts, r.te, s.ts, s.te)) c
+ *
+ * 		-- or --
+ *
+ * 		(r NORMALIZE s USING(atts) WITH (r.ts, r.te, s.ts, s.te)) c
+ * 		where q can be any join qualifier and r.ts, r.te, s.ts, and s.te
+ * 		can be any column name.
+ *
+ * OUTPUT:
+ * 		(
+ * 			SELECT r.*,
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT s.*, ts P1 FROM s
+ *      		UNION ALL
+ *      		SELECT s.*, te P1 FROM s
+ *      	) s
+ *      	ON q AND P1 >= r.ts AND P1 < r.te
+ *      	ORDER BY rn, P1
+ *      ) c
+ *
+ *      -- or --
+ *
+ * 		(
+ * 			SELECT r.*,
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT atts, ts P1 FROM s
+ *      		UNION
+ *      		SELECT atts, te P1 FROM s
+ *      	) s
+ *      	ON r.atts = s.atts AND P1 >= r.ts AND P1 < r.te
+ *      	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;
+	ResTarget			*rtLowerLarg;
+	ResTarget			*rtUpperLarg;
+	ColumnRef 			*crRargStar;
+	ColumnRef 			*crLargTsT = NULL;
+	ColumnRef 			*crRargTsT = NULL;
+	ColumnRef 			*crLargTe = NULL;
+	ColumnRef 			*crRargTe = NULL;
+	ColumnRef 			*crP1;
+	JoinExpr			*joinExpr;
+	A_Expr				*lowerBoundExpr;
+	A_Expr				*upperBoundExpr;
+	A_Expr				*containsExpr;
+	Node				*boolExpr;
+	Alias				*largAlias;
+	Alias				*rargAlias;
+	char 				*colnameRN;
+	char 				*colnameP1;
+	bool				 hasRangeTypes;
+	FuncCall			*fcLowerLarg = NULL;
+	FuncCall			*fcUpperLarg = NULL;
+	FuncCall			*fcLowerRarg = NULL;
+	FuncCall			*fcUpperRarg = NULL;
+	List				*boundariesExpr;
+
+	/* Create a select statement skeleton to be filled here */
+	ssResult = makeTemporalQuerySkeleton(j, &colnameRN, &colnameP1,
+										 NULL, &hasRangeTypes,
+										 &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. If we need only two range types
+	 * only Ts columnrefs are used.
+	 */
+	if (hasRangeTypes)
+	{
+		crLargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crRargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+
+		/* Create argument list for function call to "greatest" and "least" */
+		fcLowerLarg = makeFuncCall(SystemFuncName("lower"),
+								   list_make1(crLargTsT),
+								   UNKNOWN_LOCATION);
+		fcLowerRarg = makeFuncCall(SystemFuncName("lower"),
+								   list_make1(crRargTsT),
+								   UNKNOWN_LOCATION);
+		fcUpperLarg = makeFuncCall(SystemFuncName("upper"),
+								   list_make1(crLargTsT),
+								   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);
+
+		rtLowerLarg = makeResTarget((Node *) fcLowerLarg,
+									ssResult->temporalClause->colnameTs);
+		rtUpperLarg = makeResTarget((Node *) fcUpperLarg,
+									ssResult->temporalClause->colnameTe);
+
+		ssResult->targetList = list_concat(ssResult->targetList,
+										   list_make2(rtLowerLarg, rtUpperLarg));
+		/*
+		 * 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);
+	}
+	else
+	{
+		/*
+		 * Build column references, for use later.
+		 */
+		crLargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crLargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTE,
+										   largAlias->aliasname);
+		crRargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+		crRargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTE,
+										   rargAlias->aliasname);
+
+		/* Build resource target "ts P1" and "te P1" */
+		rtTsP1 = makeResTarget((Node *) crRargTsT, colnameP1);
+		rtTeP1 = makeResTarget((Node *) crRargTe, colnameP1);
+		/*
+		 * Build "contains" expressions, i.e. "P1 >= ts AND P1 < te"
+		 * and concatenate it with q (=theta)
+		 */
+		lowerBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  ">=",
+										  copyObject(crP1),
+										  copyObject(crLargTsT),
+										  UNKNOWN_LOCATION);
+		upperBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  "<",
+										  copyObject(crP1),
+										  copyObject(crLargTe),
+										  UNKNOWN_LOCATION);
+
+		boundariesExpr = list_make2(lowerBoundExpr, upperBoundExpr);
+	}
+
+	/*
+	 * Build "SELECT s.*, ts P1 FROM s" and "SELECT s.*, te P1 FROM s", iff we
+	 * have a ON-clause.
+	 * If we have an USING-clause with a name-list 'atts' build "SELECT atts,
+	 * ts P1 FROM s" and "SELECT atts, te 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	   *colnameTs = ssResult->temporalClause->colnameTs;
+		char	   *colnameTe = ssResult->temporalClause->colnameTe;
+		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(strcmp(usingItemName, colnameTs) == 0
+					|| strcmp(usingItemName, colnameTe) == 0
+					|| (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). These column
+ * 		names also differ depending on weither we have range types or scalars
+ * 		for temporal bounds. The list is then compared with the aliases from
+ * 		the current parser state, and renamed if necessary.
+ */
+static List *
+internalUseOnlyColumnNames(ParseState *pstate,
+						   bool hasRangeTypes,
+						   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"));
+
+	/* We split range types into upper and lower bounds, called ts and te */
+	if(hasRangeTypes)
+	{
+		filter = lappend(filter, makeString("ts"));
+		filter = lappend(filter, makeString("te"));
+	}
+
+	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. This means, if
+ * 		we have only one item in boundary list, all bounds must be range types.
+ * 		Otherwise, all bounds must be scalars.
+ */
+static void
+temporalBoundCheckIntegrity(ParseState *pstate,
+							 List *bounds,
+							 List *colnames,
+							 List *colvars,
+							 TemporalType tmpType)
+{
+	ListCell 	*lcNames;
+	ListCell 	*lcVars;
+	ListCell 	*lcBound;
+	ListCell 	*lcFilter;
+	bool 		 hasRangeTypes = list_length(bounds) == 1;
+	List		*filter = internalUseOnlyColumnNames(pstate,
+													 hasRangeTypes,
+													 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(hasRangeTypes && type->typtype != TYPTYPE_RANGE)
+					msg = "Invalid column type \"%s\" for the temporal bound " \
+						  "\"%s\". It must be a range type column.";
+
+				if(! hasRangeTypes && type->typtype == TYPTYPE_RANGE)
+					msg = "Invalid column type \"%s\" for the temporal bound " \
+						  "\"%s\". It must be a scalar type column (i.e., " \
+						  "not a range type).";
+
+				if (strlen(msg) > 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+							 errmsg(msg,
+									NameStr(type->typname),
+									NameListToString(crb->fields)),
+							 errhint("Specify four scalar columns for the " \
+									 "temporal boundaries, or 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;
+	bool 		foundTS = false;
+	bool 		foundTE = false;
+	int 		counterP1 = -1;
+	int 		counterP2 = -1;
+	int 		counterRN = -1;
+	int 		counterTS = -1;
+	int 		counterTE = -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);
+
+		if (j->inTmpPrimHasRangeT)
+		{
+			getColumnCounter(colname, "ts", &foundTS, &counterTS);
+			getColumnCounter(colname, "te", &foundTE, &counterTE);
+		}
+	}
+
+	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 (j->inTmpPrimHasRangeT)
+		{
+			getColumnCounter(colname, "ts", &foundTS, &counterTS);
+			getColumnCounter(colname, "te", &foundTE, &counterTE);
+		}
+	}
+
+	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;
+			A_Expr *e2;
+			List *qualArgs;
+			bool hasRangeTypes = list_length(j->temporalBounds) == 2;
+
+			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(! hasRangeTypes)
+			{
+				e2 = (A_Expr *) lsecond(qualArgs);
+				linitial(((ColumnRef *)e2->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);
+
+	if (foundTS)
+		addTemporalAlias(pstate, "ts", counterTS);
+
+	if (foundTE)
+		addTemporalAlias(pstate, "te", counterTE);
+
+	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;
+	j->inTmpPrimHasRangeT = list_length(bounds) == 2;
+
+	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;
+	j->inTmpPrimHasRangeT = list_length(bounds) == 2;
+
+	return j;
+}
+
diff --git src/backend/utils/adt/windowfuncs.c src/backend/utils/adt/windowfuncs.c
index 4e714cd..77fc4f1 100644
--- src/backend/utils/adt/windowfuncs.c
+++ 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 src/backend/utils/errcodes.txt src/backend/utils/errcodes.txt
index 46aadd7..7516767 100644
--- src/backend/utils/errcodes.txt
+++ src/backend/utils/errcodes.txt
@@ -204,6 +204,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 src/include/catalog/pg_proc.h src/include/catalog/pg_proc.h
index 05652e8..f6ce73a 100644
--- src/include/catalog/pg_proc.h
+++ src/include/catalog/pg_proc.h
@@ -5000,6 +5000,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 = 3999 (  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 src/include/executor/nodeTemporalAdjustment.h src/include/executor/nodeTemporalAdjustment.h
new file mode 100644
index 0000000..7a4be3d
--- /dev/null
+++ 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 src/include/nodes/execnodes.h src/include/nodes/execnodes.h
index f9bcdd6..a9c8048 100644
--- src/include/nodes/execnodes.h
+++ src/include/nodes/execnodes.h
@@ -1287,6 +1287,30 @@ 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 */
+	Form_pg_attribute     datumFormat;	  /* Datum format of sweepline, P1, P2 */
+} TemporalAdjustmentState;
+
+/* ----------------
  *	 SeqScanState information
  * ----------------
  */
diff --git src/include/nodes/makefuncs.h src/include/nodes/makefuncs.h
index 1f4bad7..ac24f95 100644
--- src/include/nodes/makefuncs.h
+++ 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 src/include/nodes/nodes.h src/include/nodes/nodes.h
index 95dd8ba..b23dca2 100644
--- src/include/nodes/nodes.h
+++ src/include/nodes/nodes.h
@@ -80,6 +80,7 @@ typedef enum NodeTag
 	T_SetOp,
 	T_LockRows,
 	T_Limit,
+	T_TemporalAdjustment,
 	/* these aren't subclasses of Plan: */
 	T_NestLoopParam,
 	T_PlanRowMark,
@@ -129,6 +130,7 @@ typedef enum NodeTag
 	T_SetOpState,
 	T_LockRowsState,
 	T_LimitState,
+	T_TemporalAdjustmentState,
 
 	/*
 	 * TAGS FOR PRIMITIVE NODES (primnodes.h)
@@ -260,6 +262,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_PartitionSpec,
 	T_PartitionBoundSpec,
 	T_PartitionRangeDatum,
+	T_TemporalClause,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
@@ -690,7 +694,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 src/include/nodes/parsenodes.h src/include/nodes/parsenodes.h
index 07a8436..4ea2c0a 100644
--- src/include/nodes/parsenodes.h
+++ src/include/nodes/parsenodes.h
@@ -162,6 +162,8 @@ typedef struct Query
 										 * 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
@@ -715,11 +717,11 @@ typedef struct XmlSerialize
 typedef struct PartitionElem
 {
 	NodeTag		type;
-	char	   *name;			/* name of column to partition on, or NULL */
-	Node	   *expr;			/* expression to partition on, or NULL */
-	List	   *collation;		/* name of collation; NIL = default */
-	List	   *opclass;		/* name of desired opclass; NIL = default */
-	int			location;		/* token location, or -1 if unknown */
+	char	   *name;		/* name of column to partition on, or NULL */
+	Node	   *expr;		/* expression to partition on, or NULL */
+	List	   *collation;	/* name of collation; NIL = default */
+	List	   *opclass;	/* name of desired opclass; NIL = default */
+	int			location;	/* token location, or -1 if unknown */
 } PartitionElem;
 
 /*
@@ -728,9 +730,9 @@ typedef struct PartitionElem
 typedef struct PartitionSpec
 {
 	NodeTag		type;
-	char	   *strategy;		/* partitioning strategy ('list' or 'range') */
-	List	   *partParams;		/* List of PartitionElems */
-	int			location;		/* token location, or -1 if unknown */
+	char	   *strategy;	/* partitioning strategy ('list' or 'range') */
+	List	   *partParams; /* List of PartitionElems */
+	int			location;	/* token location, or -1 if unknown */
 } PartitionSpec;
 
 #define PARTITION_STRATEGY_LIST		'l'
@@ -1449,6 +1451,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 src/include/nodes/plannodes.h src/include/nodes/plannodes.h
index f72f7a8..8be446d 100644
--- src/include/nodes/plannodes.h
+++ src/include/nodes/plannodes.h
@@ -220,6 +220,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 src/include/nodes/primnodes.h src/include/nodes/primnodes.h
index f72ec24..9ac0ef5 100644
--- src/include/nodes/primnodes.h
+++ src/include/nodes/primnodes.h
@@ -51,6 +51,35 @@ 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   attNumTs;
+	AttrNumber   attNumTe;
+	AttrNumber   attNumTr;
+	AttrNumber   attNumP1;
+	AttrNumber   attNumP2;
+	AttrNumber   attNumRN;
+
+	char        *colnameTs;
+	char        *colnameTe;
+	char		*colnameTr;	    /* If range type used for bounds, or NULL */
+} TemporalClause;
+
 /*
  * RangeVar - range variable, used in FROM clauses
  *
@@ -1415,6 +1444,10 @@ 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 */
+	bool		 inTmpPrimHasRangeT; /* true, if bounds are range types */
 } JoinExpr;
 
 /*----------
diff --git src/include/nodes/print.h src/include/nodes/print.h
index 4c94a3a..c6cbbf4 100644
--- src/include/nodes/print.h
+++ 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 src/include/nodes/relation.h src/include/nodes/relation.h
index 643be54..26238c6 100644
--- src/include/nodes/relation.h
+++ src/include/nodes/relation.h
@@ -1056,6 +1056,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 src/include/optimizer/pathnode.h src/include/optimizer/pathnode.h
index 7b41317..1bc562c 100644
--- src/include/optimizer/pathnode.h
+++ src/include/optimizer/pathnode.h
@@ -153,6 +153,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 src/include/parser/kwlist.h src/include/parser/kwlist.h
index 985d650..de89969 100644
--- src/include/parser/kwlist.h
+++ 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)
@@ -257,6 +258,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 src/include/parser/parse_node.h src/include/parser/parse_node.h
index 3a25d95..825c973 100644
--- src/include/parser/parse_node.h
+++ src/include/parser/parse_node.h
@@ -196,6 +196,12 @@ struct ParseState
 	bool		p_hasModifyingCTE;
 
 	/*
+	 * 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 src/include/parser/parse_temporal.h src/include/parser/parse_temporal.h
new file mode 100644
index 0000000..235831e
--- /dev/null
+++ 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 */
tpg_primitives_out_tests_v2.patchtext/x-patch; charset=US-ASCII; name=tpg_primitives_out_tests_v2.patchDownload
diff --git src/test/regress/expected/temporal_primitives.out src/test/regress/expected/temporal_primitives.out
new file mode 100644
index 0000000..6e4cc0d
--- /dev/null
+++ src/test/regress/expected/temporal_primitives.out
@@ -0,0 +1,841 @@
+--
+-- TEMPORAL PRIMITIVES
+--
+SET datestyle TO ymd;
+CREATE COLLATION "de_DE.utf8" (LC_COLLATE = "de_DE.utf8",
+                               LC_CTYPE = "de_DE.utf8" );
+CREATE TEMP TABLE tpg_table1 (a char, b char, ts int, te int);
+CREATE TEMP TABLE tpg_table2 (c int, d char, ts int, te int);
+INSERT INTO tpg_table1 VALUES
+('a','B',1,7),
+('b','B',3,9),
+('c','G',8,10);
+INSERT INTO tpg_table2 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 tpg_table3 AS
+	SELECT a, ts, te, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table4 AS
+	SELECT c, ts, d, te FROM tpg_table2;
+-- VALID TIME columns represented as range type
+CREATE TEMP TABLE tpg_table5 AS
+	SELECT int4range(ts, te) t, a, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table6 AS
+	SELECT int4range(ts, te) t, c a, d b FROM tpg_table2;
+-- VALID TIME columns as VARCHARs
+CREATE TEMP TABLE tpg_table7 (a int, ts varchar, te varchar);
+CREATE TEMP TABLE tpg_table8 (a int,
+							  ts varchar COLLATE "de_DE.utf8",
+							  te varchar COLLATE "POSIX");
+INSERT INTO tpg_table7 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X'),
+(0, 'ABC', 'BCD'),
+(0, 'xABC', 'xBCD'),
+(0, 'BAA', 'BBB');
+INSERT INTO tpg_table8 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X');
+-- Tables to check different data types, and corner cases
+CREATE TEMP TABLE tpg_table9 (a int, ts timestamp, te timestamp);
+CREATE TEMP TABLE tpg_table10 (a int, ts double precision, te double precision);
+CREATE TEMP TABLE tpg_table11 AS TABLE tpg_table10;
+INSERT INTO tpg_table9 VALUES
+(0, '2000-01-01', '2000-01-10'),
+(1, '2000-01-05', '2000-01-20');
+INSERT INTO tpg_table10 VALUES
+(0, 1.0, 1.1111),
+(1, 1.11109999, 2.0);
+INSERT INTO tpg_table11 VALUES
+(0, 1.0, 'Infinity'),
+(1, '-Infinity', 2.0);
+--
+-- TEMPORAL ALIGNER: BASICS
+--
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 ALIGN tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | ts | te | 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) x;
+ a | ts | te | 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table1 ALIGN tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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)
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 ALIGN tpg_table6
+		ON TRUE
+		WITH (t, t)
+	) x;
+   t    | a | b 
+--------+---+---
+ [1,2)  | a | B
+ [2,5)  | a | B
+ [3,4)  | a | B
+ [5,7)  | a | B
+ [3,4)  | b | B
+ [3,5)  | b | B
+ [5,7)  | b | B
+ [7,9)  | b | B
+ [8,9)  | c | G
+ [9,10) | c | G
+(10 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 tpg_table5),
+	 t2 AS (SELECT *, t v FROM tpg_table6)
+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)
+
+-- Full temporal join example with absorbing where clause, timestamp
+-- propagation (see CTEs targetlists with V and U) and scalar VALID TIME columns
+WITH t1 AS (SELECT *, ts us, te ue FROM tpg_table1),
+	 t2 AS (SELECT *, ts vs, te ve FROM tpg_table2)
+SELECT x.ts, x.te, b, a, c FROM (
+	t1 ALIGN t2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	LEFT OUTER JOIN (
+		SELECT * FROM (
+		t2 ALIGN t1
+			ON b = d
+			WITH (ts, te, ts, te)
+		) y
+	) y
+	ON b = d AND x.ts = y.ts AND x.te = y.te
+	WHERE (
+			(x.ts = us OR x.ts = vs)
+			AND
+			(x.te = ue OR x.te = ve)
+		)
+		OR us IS NULL
+		OR vs IS NULL
+	ORDER BY 1,2,3,4;
+ ts | te | b | a | c 
+----+----+---+---+---
+  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 (
+	tpg_table7 x ALIGN tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ a |  ts  |  te  
+---+------+------
+ 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)
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x ALIGN tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ERROR:  could not determine which collation to use for string comparison
+HINT:  Use the COLLATE clause to set the collation explicitly.
+--
+-- 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 (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+                                                          QUERY PLAN                                                          
+------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(tpg_table2.ts, tpg_table1.ts)), (LEAST(tpg_table2.te, tpg_table1.te))
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table2.ts < tpg_table1.te) AND (tpg_table2.te > tpg_table1.ts))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Seq Scan on tpg_table1
+(11 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+                                                          QUERY PLAN                                                          
+------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: (x.ts > 3)
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(tpg_table2.ts, tpg_table1.ts)), (LEAST(tpg_table2.te, tpg_table1.te))
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table2.ts < tpg_table1.te) AND (tpg_table2.te > tpg_table1.ts))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Seq Scan on tpg_table1
+(12 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 3;
+                                                          QUERY PLAN                                                          
+------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: ((x.c < 3) OR (x.ts > 3))
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(tpg_table2.ts, tpg_table1.ts)), (LEAST(tpg_table2.te, tpg_table1.te))
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table2.ts < tpg_table1.te) AND (tpg_table2.te > tpg_table1.ts))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                     ->  Materialize
+                           ->  Seq Scan on tpg_table1
+(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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 ALIGN tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) 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, ts, te FROM (
+	tpg_table10 t1 ALIGN tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |     ts     |   te   
+---+------------+--------
+ 0 |          1 | 1.1111
+ 0 | 1.11109999 | 1.1111
+ 1 | 1.11109999 |      2
+(3 rows)
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 ALIGN tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |    ts     |    te    
+---+-----------+----------
+ 0 |         1 |        2
+ 0 |         1 | Infinity
+ 1 | -Infinity |        2
+(3 rows)
+
+--
+-- TEMPORAL NORMALIZER: BASICS
+--
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 NORMALIZE tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | ts | te | 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 ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT * FROM (
+	tpg_table3 NORMALIZE tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) x;
+ a | ts | te | 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 ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT a, COUNT(a) FROM (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table1 NORMALIZE tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 t1 NORMALIZE tpg_table1 t2
+		USING (a)
+		WITH (ts, te, ts, te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ a | B |  1 |  7
+ b | B |  3 |  9
+ c | G |  8 | 10
+(3 rows)
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 NORMALIZE tpg_table6
+		USING (b)
+		WITH (t, t)
+	) x;
+   t    | a | b 
+--------+---+---
+ [1,2)  | a | B
+ [2,3)  | a | B
+ [3,4)  | a | B
+ [4,5)  | a | B
+ [5,7)  | a | B
+ [3,4)  | b | B
+ [4,5)  | b | B
+ [5,7)  | b | B
+ [7,9)  | b | B
+ [8,10) | c | G
+(10 rows)
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	tpg_table7 x NORMALIZE tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ a |  ts  |  te  
+---+------+------
+ 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)
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x NORMALIZE tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ERROR:  could not determine which collation to use for string comparison
+HINT:  Use the COLLATE clause to set the collation explicitly.
+--
+-- 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 (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+                                               QUERY PLAN                                                
+---------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), tpg_table1.ts
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table1.ts >= tpg_table2.ts) AND (tpg_table1.ts < tpg_table2.te))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on tpg_table1
+                                 ->  Seq Scan on tpg_table1 tpg_table1_1
+(13 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+                                               QUERY PLAN                                                
+---------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: (x.ts > 3)
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), tpg_table1.ts
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table1.ts >= tpg_table2.ts) AND (tpg_table1.ts < tpg_table2.te))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on tpg_table1
+                                 ->  Seq Scan on tpg_table1 tpg_table1_1
+(14 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 3;
+                                               QUERY PLAN                                                
+---------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: ((x.c < 3) OR (x.ts > 3))
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), tpg_table1.ts
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table1.ts >= tpg_table2.ts) AND (tpg_table1.ts < tpg_table2.te))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on tpg_table1
+                                 ->  Seq Scan on tpg_table1 tpg_table1_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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 NORMALIZE tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) 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, ts, te FROM (
+	tpg_table10 t1 NORMALIZE tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |     ts     |     te     
+---+------------+------------
+ 0 |          1 | 1.11109999
+ 0 | 1.11109999 |     1.1111
+ 1 | 1.11109999 |          2
+(3 rows)
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 NORMALIZE tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |    ts     |    te    
+---+-----------+----------
+ 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 (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+TABLE v;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+TABLE v;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) NORMALIZE tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1", p1_1, ts, te)
+	) x;
+TABLE v;
+ p1 | p1_0 | p1_-1 | 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) ALIGN tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1",p1_1,ts,te)
+	) x;
+TABLE v;
+ p1 | p1_0 | p1_-1 | 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 src/test/regress/parallel_schedule src/test/regress/parallel_schedule
index edeb2d6..0dc8ad1 100644
--- src/test/regress/parallel_schedule
+++ 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
+test: alter_generic alter_operator misc psql async dbsize misc_functions sysviews tsrf temporal_primitives
 
 # rules cannot run concurrently with any test that creates a view
 test: rules psql_crosstab amutils
diff --git src/test/regress/sql/temporal_primitives.sql src/test/regress/sql/temporal_primitives.sql
new file mode 100644
index 0000000..9b475f7
--- /dev/null
+++ src/test/regress/sql/temporal_primitives.sql
@@ -0,0 +1,456 @@
+--
+-- TEMPORAL PRIMITIVES
+--
+SET datestyle TO ymd;
+
+CREATE COLLATION "de_DE.utf8" (LC_COLLATE = "de_DE.utf8",
+                               LC_CTYPE = "de_DE.utf8" );
+
+CREATE TEMP TABLE tpg_table1 (a char, b char, ts int, te int);
+CREATE TEMP TABLE tpg_table2 (c int, d char, ts int, te int);
+
+INSERT INTO tpg_table1 VALUES
+('a','B',1,7),
+('b','B',3,9),
+('c','G',8,10);
+INSERT INTO tpg_table2 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 tpg_table3 AS
+	SELECT a, ts, te, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table4 AS
+	SELECT c, ts, d, te FROM tpg_table2;
+
+-- VALID TIME columns represented as range type
+CREATE TEMP TABLE tpg_table5 AS
+	SELECT int4range(ts, te) t, a, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table6 AS
+	SELECT int4range(ts, te) t, c a, d b FROM tpg_table2;
+
+-- VALID TIME columns as VARCHARs
+CREATE TEMP TABLE tpg_table7 (a int, ts varchar, te varchar);
+CREATE TEMP TABLE tpg_table8 (a int,
+							  ts varchar COLLATE "de_DE.utf8",
+							  te varchar COLLATE "POSIX");
+
+INSERT INTO tpg_table7 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X'),
+(0, 'ABC', 'BCD'),
+(0, 'xABC', 'xBCD'),
+(0, 'BAA', 'BBB');
+
+INSERT INTO tpg_table8 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X');
+
+-- Tables to check different data types, and corner cases
+CREATE TEMP TABLE tpg_table9 (a int, ts timestamp, te timestamp);
+CREATE TEMP TABLE tpg_table10 (a int, ts double precision, te double precision);
+CREATE TEMP TABLE tpg_table11 AS TABLE tpg_table10;
+
+INSERT INTO tpg_table9 VALUES
+(0, '2000-01-01', '2000-01-10'),
+(1, '2000-01-05', '2000-01-20');
+
+INSERT INTO tpg_table10 VALUES
+(0, 1.0, 1.1111),
+(1, 1.11109999, 2.0);
+
+INSERT INTO tpg_table11 VALUES
+(0, 1.0, 'Infinity'),
+(1, '-Infinity', 2.0);
+
+
+--
+-- TEMPORAL ALIGNER: BASICS
+--
+
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+
+-- Alignment with aggregation
+-- NB: Targetlist of outer query is *not* A_STAR...
+SELECT a, COUNT(a) FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	GROUP BY a ORDER BY a;
+
+-- Test relations with differently named temporal bound attributes and relation
+-- and column aliases.
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 ALIGN tpg_table6
+		ON TRUE
+		WITH (t, t)
+	) 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 tpg_table5),
+	 t2 AS (SELECT *, t v FROM tpg_table6)
+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;
+
+-- Full temporal join example with absorbing where clause, timestamp
+-- propagation (see CTEs targetlists with V and U) and scalar VALID TIME columns
+WITH t1 AS (SELECT *, ts us, te ue FROM tpg_table1),
+	 t2 AS (SELECT *, ts vs, te ve FROM tpg_table2)
+SELECT x.ts, x.te, b, a, c FROM (
+	t1 ALIGN t2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	LEFT OUTER JOIN (
+		SELECT * FROM (
+		t2 ALIGN t1
+			ON b = d
+			WITH (ts, te, ts, te)
+		) y
+	) y
+	ON b = d AND x.ts = y.ts AND x.te = y.te
+	WHERE (
+			(x.ts = us OR x.ts = vs)
+			AND
+			(x.te = ue OR x.te = ve)
+		)
+		OR us IS NULL
+		OR vs IS NULL
+	ORDER BY 1,2,3,4;
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	tpg_table7 x ALIGN tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x ALIGN tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 ALIGN tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision
+SELECT a, ts, te FROM (
+	tpg_table10 t1 ALIGN tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 ALIGN tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+
+--
+-- TEMPORAL NORMALIZER: BASICS
+--
+
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+
+-- Alignment with aggregation
+-- NB: Targetlist of outer query is *not* A_STAR...
+SELECT a, COUNT(a) FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	GROUP BY a ORDER BY a;
+
+-- Test relations with differently named temporal bound attributes and relation
+-- and column aliases.
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+
+-- Normalizer's USING clause (self-normalization)
+SELECT * FROM (
+	tpg_table1 t1 NORMALIZE tpg_table1 t2
+		USING (a)
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 NORMALIZE tpg_table6
+		USING (b)
+		WITH (t, t)
+	) x;
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	tpg_table7 x NORMALIZE tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x NORMALIZE tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 NORMALIZE tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision
+SELECT a, ts, te FROM (
+	tpg_table10 t1 NORMALIZE tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 NORMALIZE tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+--
+-- TEMPORAL ALIGNER AND NORMALIZER: VIEWS
+--
+
+-- Views with temporal normalization
+CREATE TEMP VIEW v AS SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+
+TABLE v;
+DROP VIEW v;
+
+-- Views with temporal alignment
+CREATE TEMP VIEW v AS SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) NORMALIZE tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1", p1_1, ts, te)
+	) 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) ALIGN tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1",p1_1,ts,te)
+	) x;
+
+TABLE v;
+DROP VIEW v;
+
+
#16Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Peter Moser (#15)
Re: [PROPOSAL] Temporal query processing with range types

On 2/2/17 12:43 PM, Peter Moser wrote:

Hereby, we used the following commands to create both patches:
git diff --no-prefix origin/master -- src/ ':!src/test/*' >
tpg_primitives_out_v4.patch

git diff --no-prefix origin/master -- src/test/ >
tpg_primitives_out_tests_v2.patch

We have also tested our patches on the current HEAD with the command:
patch -p0 < patch-file

Both worked without problems or warnings on our Linux machine.
Could you please explain, which problems occurred while you tried to
apply our patches?

Your patches apply OK for me.

In the future, just use git diff without any options or git
format-patch, and put both the code and the tests in one patch.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#17Robert Haas
robertmhaas@gmail.com
In reply to: Peter Moser (#13)
Re: [PROPOSAL] Temporal query processing with range types

On Tue, Jan 24, 2017 at 4:32 AM, Peter Moser <pitiz29a@gmail.com> wrote:

NORMALIZE: splits all the ranges of one relation according to all the range
boundaries of another (but possibly the same) relation whenever some equality
condition over some given attributes is satisfied.

When the two relations are the same, all ranges with the given equal attributes
are either equal or disjoint. After this, the traditional GROUP BY or DISTINCT
can be applied. The attributes given to NORMALIZE for a (temporal) GROUP BY are
the grouping attributes and for a (temporal) DISTINCT the target list
attributes.

When the two relations are different, but they each contain disjoint ranges
for the same attributes (as the current limitation for the set operations is)
we perform a symmetric NORMALIZE on each of them. Then we have a similar
situation as before, i.e., in both relations ranges with the same attributes
are either equal or disjoint and a traditional set operation
(EXCEPT/INTERSECT/UNION) can be applied. The attributes given to NORMALIZE for
a (temporal) EXCEPT/INTERSECT/UNION are the target list attributes.

ALIGN: splits all the ranges of one relation according to all the range
intersections of another relation, i.e., it produces all intersections and
non-overlapping parts, whenever some condition is satisfied.

We perform a symmetric ALIGN on each relation, after which a traditional inner
or outer join can be applied using equality on the ranges to calculate the
overlap. The condition given to a (temporal) inner or outer join is the
join condition without overlap.

I don't quite understand the difference between NORMALIZE and ALIGN.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#18Robert Haas
robertmhaas@gmail.com
In reply to: Peter Moser (#13)
Re: [PROPOSAL] Temporal query processing with range types

On Tue, Jan 24, 2017 at 4:32 AM, Peter Moser <pitiz29a@gmail.com> wrote:

Using common terms such as ALIGN and NORMALIZE for such a specific
functionality seems a bit wrong.

Would ALIGN RANGES/RANGE ALIGN and NORMALIZE RANGES/RANGE NORMALIZE be better
options? We are also thankful for any suggestion or comments about the syntax.

So it seems like an ALIGN or NORMALIZE option is kind of like a JOIN,
except apparently there's no join type and the optimizer can never
reorder these operations with each other or with other joins. Is that
right? The optimizer changes in this patch seem fairly minimal, so
I'm guessing it can't be doing anything very complex here.

What happens if you perform the ALIGN or NORMALIZE operation using
something other than an equality operator, like, say, less-than? Or
an arbitrary user-defined operator.

There's no documentation in this patch. I'm not sure you want to go
to the trouble of writing SGML documentation until this has been
reviewed enough that it has a real chance of getting committed, but on
the other hand we're obviously all struggling to understand what it
does, so I think if not SGML documentation it at least needs a real
clear explanation of what the syntax is and does in a README or
something, even just for initial review.

We don't have anything quite like this in PostgreSQL today. An
ordinary join just matches up things in relation A and relation B and
outputs the matching rows, and something like a SRF takes a set of
input rows and returns a set of output rows. This is a hybrid - it
takes in two sets of rows, one from each relation being joined, and
produces a derived set of output rows that takes into account both
inputs.

+ * INPUT:
+ *             (r ALIGN s ON q WITH (r.ts, r.te, s.ts, s.te)) c
+ *             where q can be any join qualifier, and r.ts, r.te, s.ts, and s.t
e
+ *             can be any column name.
+ *
+ * OUTPUT:
+ *             (
+ *             SELECT r.*, GREATEST(r.ts, s.ts) P1, LEAST(r.te, s.te) P2
+ *      FROM
+ *      (
+ *             SELECT *, row_id() OVER () rn FROM r
+ *      ) r
+ *      LEFT OUTER JOIN
+ *      s
+ *      ON q AND r.ts < s.te AND r.te > s.ts
+ *      ORDER BY rn, P1, P2
+ *      ) c

It's hard to see what's going on here. What's ts? What's te? If you
used longer names for these things, it might be a bit more
self-documenting.

If we are going to transform an ALIGN operator in to a left outer
join, why do we also have an executor node for it?

+               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);

Why is a temporal operator calling functions that upper-case and
lower-case strings? In one sense this whole function (and much of the
nearby code) is very straightforward code and you can see exactly why
it's doing it. In another sense it's totally inscrutable: WHY is it
doing any of that stuff?

-       char       *strategy;           /* partitioning strategy
('list' or 'range') */
-       List       *partParams;         /* List of PartitionElems */
-       int                     location;               /* token
location, or -1 if unknown */
+       char       *strategy;   /* partitioning strategy ('list' or 'range') */
+       List       *partParams; /* List of PartitionElems */
+       int                     location;       /* token location, or
-1 if unknown */

I think this is some kind of mistake on your end while generating the
patch. It looks like you patched one version of the source code, and
diffed against another.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#19David G. Johnston
david.g.johnston@gmail.com
In reply to: Robert Haas (#18)
Re: [PROPOSAL] Temporal query processing with range types

On Wed, Feb 15, 2017 at 12:24 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, Jan 24, 2017 at 4:32 AM, Peter Moser <pitiz29a@gmail.com> wrote:

Using common terms such as ALIGN and NORMALIZE for such a specific
functionality seems a bit wrong.

Would ALIGN RANGES/RANGE ALIGN and NORMALIZE RANGES/RANGE NORMALIZE be

better

options? We are also thankful for any suggestion or comments about the

syntax.

So it seems like an ALIGN or NORMALIZE option is kind of like a JOIN,
except apparently there's no join type and the optimizer can never
reorder these operations with each other or with other joins. Is that
right? The optimizer changes in this patch seem fairly minimal, so
I'm guessing it can't be doing anything very complex here.

+ * INPUT:
+ *             (r ALIGN s ON q WITH (r.ts, r.te, s.ts, s.te)) c
+ *             where q can be any join qualifier, and r.ts, r.te, s.ts,
and s.t
e
+ *             can be any column name.
+ *
+ * OUTPUT:
+ *             (
+ *             SELECT r.*, GREATEST(r.ts, s.ts) P1, LEAST(r.te, s.te) P2
+ *      FROM
+ *      (
+ *             SELECT *, row_id() OVER () rn FROM r
+ *      ) r
+ *      LEFT OUTER JOIN
+ *      s
+ *      ON q AND r.ts < s.te AND r.te > s.ts
+ *      ORDER BY rn, P1, P2
+ *      ) c

It's hard to see what's going on here. What's ts? What's te? If you
used longer names for these things, it might be a bit more
self-documenting.

Just reasoning out loud here...​

ISTM ts and te are "temporal [range] start" and "temporal [range] end"​ (or
probably just the common "timestamp start/end")

​From what I can see it is affecting an intersection of the two ranges and,
furthermore, splitting the LEFT range into sub-ranges that match up with
the sub-ranges found on the right side. From the example above this seems
like it should be acting on self-normalized ranges - but I may be missing
something by evaluating this out of context.

r1 [1, 6] [ts, te] [time period start, time period end]
s1 [2, 3]
s2 [3, 4]
s3 [5, 7]

r LEFT JOIN s ON (r.ts < s.te AND r.te > s.ts)

r1[1, 6],s1[2, 3] => [max(r.ts, s.ts),min(r.te, s.te)] => r1[1, 6],d[2, 3]
r1[1, 6],s2[3, 4] => [max(t.ts, s.ts),min(r.te, s.te)] => r1[1, 6],d[3, 4]
r1[1, 6],s3[5, 7] => [max(t.ts, s.ts),min(r.te, s.te)] => r1[1, 6],d[5, 6]

Thus the intersection is [2,6] but since s1 has three ranges that begin
between 2 and 6 (i.e., 2, 3, and 5) there are three output records that
correspond to those sub-ranges.

The description in the OP basically distinguishes between NORMALIZE and
ALIGN in that ALIGN, as described above, affects an INTERSECTION on the two
ranges - discarding the non-overlapping data - while NORMALIZE performs the
alignment while also retaining the non-overlapping data.

The rest of the syntax seems to deal with selecting subsets of range
records based upon attribute data.

David J.

#20Robert Haas
robertmhaas@gmail.com
In reply to: David G. Johnston (#19)
Re: [PROPOSAL] Temporal query processing with range types

On Wed, Feb 15, 2017 at 3:33 PM, David G. Johnston
<david.g.johnston@gmail.com> wrote:

The description in the OP basically distinguishes between NORMALIZE and
ALIGN in that ALIGN, as described above, affects an INTERSECTION on the two
ranges - discarding the non-overlapping data - while NORMALIZE performs the
alignment while also retaining the non-overlapping data.

Hmm. So is ALIGN like an inner join and NORMALIZE like a full outer
join? Couldn't you have left and right outer joins, too, like you
null-extend the parts of the lefthand range that don't match but throw
away parts of the righthand range that don't match?

Also, it sounds like all of this is intended to work with ranges that
are stored in different columns rather than with PostgreSQL's built-in
range types.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#21Peter Moser
pitiz29a@gmail.com
In reply to: David G. Johnston (#19)
Re: [PROPOSAL] Temporal query processing with range types

On Wed, Feb 15, 2017 at 9:33 PM, David G. Johnston
<david.g.johnston@gmail.com> wrote:

On Wed, Feb 15, 2017 at 12:24 PM, Robert Haas <robertmhaas@gmail.com> wrote:

So it seems like an ALIGN or NORMALIZE option is kind of like a JOIN,
except apparently there's no join type and the optimizer can never
reorder these operations with each other or with other joins. Is that
right? The optimizer changes in this patch seem fairly minimal, so
I'm guessing it can't be doing anything very complex here.

+ * INPUT:
+ *             (r ALIGN s ON q WITH (r.ts, r.te, s.ts, s.te)) c
+ *             where q can be any join qualifier, and r.ts, r.te, s.ts,
and s.t
e
+ *             can be any column name.
+ *
+ * OUTPUT:
+ *             (
+ *             SELECT r.*, GREATEST(r.ts, s.ts) P1, LEAST(r.te, s.te) P2
+ *      FROM
+ *      (
+ *             SELECT *, row_id() OVER () rn FROM r
+ *      ) r
+ *      LEFT OUTER JOIN
+ *      s
+ *      ON q AND r.ts < s.te AND r.te > s.ts
+ *      ORDER BY rn, P1, P2
+ *      ) c

It's hard to see what's going on here. What's ts? What's te? If you
used longer names for these things, it might be a bit more
self-documenting.

Just reasoning out loud here...

ISTM ts and te are "temporal [range] start" and "temporal [range] end" (or
probably just the common "timestamp start/end")

From what I can see it is affecting an intersection of the two ranges and,
furthermore, splitting the LEFT range into sub-ranges that match up with the
sub-ranges found on the right side. From the example above this seems like
it should be acting on self-normalized ranges - but I may be missing
something by evaluating this out of context.

r1 [1, 6] [ts, te] [time period start, time period end]
s1 [2, 3]
s2 [3, 4]
s3 [5, 7]

r LEFT JOIN s ON (r.ts < s.te AND r.te > s.ts)

r1[1, 6],s1[2, 3] => [max(r.ts, s.ts),min(r.te, s.te)] => r1[1, 6],d[2, 3]
r1[1, 6],s2[3, 4] => [max(t.ts, s.ts),min(r.te, s.te)] => r1[1, 6],d[3, 4]
r1[1, 6],s3[5, 7] => [max(t.ts, s.ts),min(r.te, s.te)] => r1[1, 6],d[5, 6]

Thus the intersection is [2,6] but since s1 has three ranges that begin
between 2 and 6 (i.e., 2, 3, and 5) there are three output records that
correspond to those sub-ranges.

Yes, this is what the internal rewriting produces for r1.
Note that till now we only support half-open ranges, i.e., [), but for
visibility I will continue this example using closed ranges [].
The executor function (ExecTemporalAdjustment) gets this (the output above) as
the input and will then produce:

r1[1, 1]
r1[2, 3]
r1[3, 4]
r1[5, 6]

Which means also for the ALIGN the non-overlapping parts are retained.

The description in the OP basically distinguishes between NORMALIZE and
ALIGN in that ALIGN, as described above, affects an INTERSECTION on the two
ranges - discarding the non-overlapping data - while NORMALIZE performs the
alignment while also retaining the non-overlapping data.

Also for ALIGN we retain the non-overlapping part.
Intersections are symmetric/commutative, so a subsequent outer join can then use
equality on the ranges
to produce join matches (for overlapping) as well as null-extend the
produced non-overlapping parts.

The difference between ALIGN and NORMALIZE is how they split, while ALIGN
produces intersections between pairs of tuples (used for joins) and the
non-overlapping parts, NORMALIZE produces intersections between groups of tuples
(used for aggregation, so that all tuples with the same group have equal
ranges) and the non-overlapping parts.

For instance, the NORMALIZE between r1, s1, s2, and s3 in your example above
would give the following:

r1[1, 1]
r1[2, 2]
r1[3, 3]
r1[4, 4]
r1[5, 6]

The rest of the syntax seems to deal with selecting subsets of range records
based upon attribute data.

Yes, exactly!

Best regards,
Anton, Johann, Michael, Peter

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#22Peter Moser
pitiz29a@gmail.com
In reply to: Robert Haas (#20)
1 attachment(s)
Re: [PROPOSAL] Temporal query processing with range types

2017-02-15 20:24 GMT+01:00 Robert Haas <robertmhaas@gmail.com>:

So it seems like an ALIGN or NORMALIZE option is kind of like a JOIN,
except apparently there's no join type and the optimizer can never
reorder these operations with each other or with other joins. Is that
right? The optimizer changes in this patch seem fairly minimal, so
I'm guessing it can't be doing anything very complex here.

ALIGN/NORMALIZE operators are aliased from-clause items, which get rewritten
into a subquery using LEFT OUTER JOIN. The main idea behind that
is to reuse as much as possible of the existing PostgreSQL code, and
just provide
one executor function to process the output of these rewritten
queries. The optimizer
code is minimal, because we do not use any new constructs (except the
temporal adjustment
node for align/normalize) due to these rewrites. That is, all needed
optimization
techniques are already present.

What happens if you perform the ALIGN or NORMALIZE operation using
something other than an equality operator, like, say, less-than? Or
an arbitrary user-defined operator.

It is possible to use ALIGN/NORMALIZE with user-defined functions,
and non-equality operators.

There's no documentation in this patch. I'm not sure you want to go
to the trouble of writing SGML documentation until this has been
reviewed enough that it has a real chance of getting committed, but on
the other hand we're obviously all struggling to understand what it
does, so I think if not SGML documentation it at least needs a real
clear explanation of what the syntax is and does in a README or
something, even just for initial review.

We are currently writing SGML documentation and extend in-code comments.
Both, will be send soon.

It's hard to see what's going on here. What's ts? What's te? If you
used longer names for these things, it might be a bit more
self-documenting.

ts, te describe an half-open interval in which the tuple is considered valid:

[time point start, time point end).

We have added an extended version of comments for both parser functions, i.e.,
transformTemporalAligner and transformTemporalNormalizer. See attached patch
(src/backend/parser/parse_temporal.c).

If we are going to transform an ALIGN operator in to a left outer
join, why do we also have an executor node for it?

+               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);

Why is a temporal operator calling functions that upper-case and
lower-case strings? In one sense this whole function (and much of the
nearby code) is very straightforward code and you can see exactly why
it's doing it. In another sense it's totally inscrutable: WHY is it
doing any of that stuff?

These functions extract the lower-bound, and upper-bound of range types.

-       char       *strategy;           /* partitioning strategy
('list' or 'range') */
-       List       *partParams;         /* List of PartitionElems */
-       int                     location;               /* token
location, or -1 if unknown */
+       char       *strategy;   /* partitioning strategy ('list' or 'range') */
+       List       *partParams; /* List of PartitionElems */
+       int                     location;       /* token location, or
-1 if unknown */

I think this is some kind of mistake on your end while generating the
patch. It looks like you patched one version of the source code, and
diffed against another.

Thank you for pointing this out, we fixed it in our new patch.

2017-02-16 13:41 GMT+01:00 Robert Haas <robertmhaas@gmail.com>:

Also, it sounds like all of this is intended to work with ranges that
are stored in different columns rather than with PostgreSQL's built-in
range types.

Our syntax supports PostgreSQL's built-in range types and ranges that
are stored in different columns.

For instance, for range types an ALIGN query would look like this:
SELECT * FROM (r ALIGN s ON q WITH (r.t, s.t)) c

... and for ranges in different columns like this:
SELECT * FROM (r ALIGN s ON q WITH (r.ts, r.te, s.ts, s.te)) c

... where r and s are input relations, q can be any join qualifier, and
r.t, s.t, r.ts, r.te, s.ts, and s.te 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 can
be defined as four scalars, or two half-open, i.e., [), range typed
values.

Best regards,
Anton, Johann, Michael, Peter

ps. The patch has been rebased on top of
commit 090f21bbad98001979da8589e9647a1d49bce4ee
from "Sun Feb 19 17:18:10 2017 -0500"

Attachments:

tpg_primitives_out_v5.patchtext/x-patch; charset=US-ASCII; name=tpg_primitives_out_v5.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c9e0a3e..f7056e9 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -899,6 +899,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 2a2b7eb..490a1c6 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -26,6 +26,8 @@ OBJS = execAmi.o execCurrent.o execGrouping.o execIndexing.o execJunk.o \
        nodeSamplescan.o nodeSeqscan.o nodeSetOp.o nodeSort.o nodeUnique.o \
        nodeValuesscan.o nodeCtescan.o nodeWorktablescan.o \
        nodeGroup.o nodeSubplan.o nodeSubqueryscan.o nodeTidscan.o \
-       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o
+       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.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 0dd95c6..9fe738c 100644
--- a/src/backend/executor/execProcnode.c
+++ b/src/backend/executor/execProcnode.c
@@ -115,6 +115,7 @@
 #include "executor/nodeValuesscan.h"
 #include "executor/nodeWindowAgg.h"
 #include "executor/nodeWorktablescan.h"
+#include "executor/nodeTemporalAdjustment.h"
 #include "nodes/nodeFuncs.h"
 #include "miscadmin.h"
 
@@ -340,6 +341,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 */
@@ -541,6 +547,10 @@ ExecProcNode(PlanState *node)
 			result = ExecLimit((LimitState *) node);
 			break;
 
+		case T_TemporalAdjustmentState:
+			result = ExecTemporalAdjustment((TemporalAdjustmentState *) node);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
 			result = NULL;
@@ -793,6 +803,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;
@@ -826,3 +840,4 @@ ExecShutdownNode(PlanState *node)
 
 	return planstate_tree_walker(node, ExecShutdownNode, NULL);
 }
+
diff --git a/src/backend/executor/nodeTemporalAdjustment.c b/src/backend/executor/nodeTemporalAdjustment.c
new file mode 100644
index 0000000..2afe0ec
--- /dev/null
+++ b/src/backend/executor/nodeTemporalAdjustment.c
@@ -0,0 +1,536 @@
+#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 */
+
+/*
+ * #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);
+}
+
+/*
+ * 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->attNumTs - 1] = newTs;
+	node->newValues[node->temporalCl->attNumTe - 1] = newTe;
+	if(node->temporalCl->attNumTr != -1)
+		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.ts, r.te, s.ts, s.te)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1, P2)
+ * 	  where r_1,...,r_n are all attributes from relation r. Two of these
+ * 	  attributes are temporal boundaries, namely TS and TE. The interval
+ * 	  [TS,TE) 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.ts, r.te, s.ts, s.te)) x
+ * 	  or
+ * 	  ... FROM (r NORMALIZE s ON theta WITH (r.ts, r.te, s.ts, s.te)) x
+ *
+ * 	Input: x(r_1, ..., r_n, RN, P1)
+ * 	  where r_1,...,r_n are all attributes from relation r. Two of these
+ * 	  attributes are temporal boundaries, namely TS and TE. The interval
+ * 	  [TS,TE) represents the VALID TIME of each tuple. RN is the
+ * 	  temporal group ID or row number, and P1 is union of both
+ * 	  timepoints TS and TE 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;
+	Datum 				 currP2;
+	Datum				 currRN;
+	Datum				 prevRN;
+	Datum				 prevTe;
+
+	if(node->firstCall)
+	{
+		curr = ExecProcNode(outerPlan);
+		if(TupIsNull(curr))
+			return NULL;
+
+		prev = ExecCopySlot(prev, curr);
+		node->sameleft = true;
+		node->firstCall = false;
+		node->outrn = 0;
+		node->datumFormat = curr->tts_tupleDescriptor->attrs[tc->attNumTs - 1];
+		setSweepline(slotGetAttrNotNull(curr, tc->attNumTs));
+	}
+
+	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(heapGetAttrNotNull(out, tc->attNumTs),
+									currP1,
+									node)
+						|| !isEqual(heapGetAttrNotNull(out, tc->attNumTe),
+									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 = heapGetAttrNotNull(prev, tc->attNumTe);
+
+			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(slotGetAttrNotNull(curr, tc->attNumTs));
+			}
+			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));
+
+	/* 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;
+
+	/*
+	 * Miscellaneous initialization
+	 *
+	 * Unique 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.
+	 */
+	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 align 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->attNumTs - 1] = true;
+	state->tsteMask[state->temporalCl->attNumTe - 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);
+
+	/*
+	 * Range types in boundaries need special treatment:
+	 * - there is an extra column in each tuple that must be changed
+	 * - and a range constructor method that must be called
+	 */
+	if(node->temporalCl->attNumTr != -1)
+	{
+		state->tsteMask[state->temporalCl->attNumTr - 1] = true;
+		fmgr_info(fmgr_internal_function("range_constructor2"), rcFunctionInfo);
+		rcFunctionInfo->fn_expr = (fmNodePtr) node->rangeVar;
+		InitFunctionCallInfoData(state->rcFuncCallInfo, rcFunctionInfo, 2,
+								 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));
+}
+
+
+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 05d8538..e55d7e1 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1968,6 +1968,9 @@ _copyJoinExpr(const JoinExpr *from)
 	COPY_NODE_FIELD(quals);
 	COPY_NODE_FIELD(alias);
 	COPY_SCALAR_FIELD(rtindex);
+	COPY_NODE_FIELD(temporalBounds);
+	COPY_SCALAR_FIELD(inTmpPrimTempType);
+	COPY_SCALAR_FIELD(inTmpPrimHasRangeT);
 
 	return newnode;
 }
@@ -2329,6 +2332,45 @@ _copyOnConflictClause(const OnConflictClause *from)
 	return newnode;
 }
 
+static TemporalClause *
+_copyTemporalClause(const TemporalClause *from)
+{
+	TemporalClause *newnode = makeNode(TemporalClause);
+
+	COPY_SCALAR_FIELD(temporalType);
+	COPY_SCALAR_FIELD(attNumTs);
+	COPY_SCALAR_FIELD(attNumTe);
+	COPY_SCALAR_FIELD(attNumTr);
+	COPY_SCALAR_FIELD(attNumP1);
+	COPY_SCALAR_FIELD(attNumP2);
+	COPY_SCALAR_FIELD(attNumRN);
+	COPY_STRING_FIELD(colnameTs);
+	COPY_STRING_FIELD(colnameTe);
+	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)
 {
@@ -2788,6 +2830,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);
 
@@ -2869,6 +2912,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);
@@ -5286,6 +5330,12 @@ copyObject(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 d595cd7..d2264ec 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -756,6 +756,9 @@ _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);
+	COMPARE_SCALAR_FIELD(inTmpPrimHasRangeT);
 
 	return true;
 }
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 7586cce..0a2297f 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 -
@@ -610,3 +693,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 b3802b4..a01dbfc 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1559,6 +1559,9 @@ _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);
+	WRITE_BOOL_FIELD(inTmpPrimHasRangeT);
 }
 
 static void
@@ -1837,6 +1840,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");
@@ -2525,6 +2540,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);
@@ -2601,6 +2617,38 @@ _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(attNumTs);
+	WRITE_INT_FIELD(attNumTe);
+	WRITE_INT_FIELD(attNumTr);
+	WRITE_INT_FIELD(attNumP1);
+	WRITE_INT_FIELD(attNumP2);
+	WRITE_INT_FIELD(attNumRN);
+	WRITE_STRING_FIELD(colnameTs);
+	WRITE_STRING_FIELD(colnameTe);
+	WRITE_STRING_FIELD(colnameTr);
+}
+
+static void
 _outColumnDef(StringInfo str, const ColumnDef *node)
 {
 	WRITE_NODE_TYPE("COLUMNDEF");
@@ -2732,6 +2780,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);
@@ -3707,6 +3756,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;
@@ -3804,6 +3856,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);
diff --git a/src/backend/nodes/print.c b/src/backend/nodes/print.c
index 926f226..e007947 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"
 
 
 /*
@@ -492,3 +493,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 d2f69fe..79a7c00 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,28 @@ _readSetOperationStmt(void)
 	READ_DONE();
 }
 
+/*
+ * _readTemporalClause
+ */
+static TemporalClause *
+_readTemporalClause(void)
+{
+	READ_LOCALS(TemporalClause);
+
+	READ_ENUM_FIELD(temporalType, TemporalType);
+	READ_INT_FIELD(attNumTs);
+	READ_INT_FIELD(attNumTe);
+	READ_INT_FIELD(attNumTr);
+	READ_INT_FIELD(attNumP1);
+	READ_INT_FIELD(attNumP2);
+	READ_INT_FIELD(attNumRN);
+	READ_STRING_FIELD(colnameTs);
+	READ_STRING_FIELD(colnameTe);
+	READ_STRING_FIELD(colnameTr);
+
+	READ_DONE();
+}
+
 
 /*
  *	Stuff from primnodes.h.
@@ -460,6 +483,17 @@ _readRangeVar(void)
 	READ_DONE();
 }
 
+static ColumnRef *
+_readColumnRef(void)
+{
+	READ_LOCALS(ColumnRef);
+
+	READ_NODE_FIELD(fields);
+	READ_LOCATION_FIELD(location);
+
+	READ_DONE();
+}
+
 static IntoClause *
 _readIntoClause(void)
 {
@@ -1241,6 +1275,9 @@ _readJoinExpr(void)
 	READ_NODE_FIELD(quals);
 	READ_NODE_FIELD(alias);
 	READ_INT_FIELD(rtindex);
+	READ_NODE_FIELD(temporalBounds);
+	READ_ENUM_FIELD(inTmpPrimTempType, TemporalType);
+	READ_BOOL_FIELD(inTmpPrimHasRangeT);
 
 	READ_DONE();
 }
@@ -2458,6 +2495,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 eeacf81..c92ea97 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 */
@@ -127,6 +128,7 @@ static void subquery_push_qual(Query *subquery,
 static void recurse_push_qual(Node *setOp, Query *topquery,
 				  RangeTblEntry *rte, Index rti, Node *qual);
 static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
+static bool allWindowFuncsHaveRowId(List *targetList);
 
 
 /*
@@ -2355,7 +2357,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;
 
 	/*
@@ -2465,6 +2468,7 @@ static void
 check_output_expressions(Query *subquery, pushdown_safety_info *safetyInfo)
 {
 	ListCell   *lc;
+	bool wfsafe = allWindowFuncsHaveRowId(subquery->targetList);
 
 	foreach(lc, subquery->targetList)
 	{
@@ -2503,12 +2507,35 @@ 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 resnoTs =
+					((TemporalClause *)subquery->temporalClause)->attNumTs;
+			AttrNumber resnoTe =
+					((TemporalClause *)subquery->temporalClause)->attNumTe;
+			AttrNumber resnoRangeT =
+					((TemporalClause *)subquery->temporalClause)->attNumTr;
+
+			if (tle->resno == resnoTs
+					|| tle->resno == resnoTe
+					|| tle->resno == resnoRangeT)
+			{
+				safetyInfo->unsafeColumns[tle->resno] = true;
+				continue;
+			}
+		}
 	}
 }
 
@@ -2578,6 +2605,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
@@ -2821,6 +2874,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 997bdcf..6142679 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,
@@ -272,6 +275,9 @@ static ModifyTable *make_modifytable(PlannerInfo *root,
 				 List *resultRelations, List *subplans,
 				 List *withCheckOptionLists, List *returningLists,
 				 List *rowMarks, OnConflictExpr *onconflict, int epqParam);
+static TemporalAdjustment *make_temporalAdjustment(Plan *lefttree,
+						TemporalClause *temporalClause,
+						List *sortClause);
 
 
 /*
@@ -469,6 +475,11 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
 											  (LimitPath *) best_path,
 											  flags);
 			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);
@@ -2277,6 +2288,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;
+}
+
 
 /*****************************************************************************
  *
@@ -4894,6 +4932,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 = 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 3d33d46..187647f 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1972,6 +1972,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
@@ -4288,7 +4302,6 @@ create_ordered_paths(PlannerInfo *root,
 	return ordered_rel;
 }
 
-
 /*
  * make_group_input_target
  *	  Generate appropriate PathTarget for initial input to grouping nodes.
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index be267b9..bc93f33 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -612,6 +612,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 7954c44..17379d0 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -2697,6 +2697,7 @@ finalize_plan(PlannerInfo *root, Plan *plan, Bitmapset *valid_params,
 		case T_Gather:
 		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 3248296..a5479c0 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2425,6 +2425,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 df9a9fb..2d64e48 100644
--- a/src/backend/parser/Makefile
+++ b/src/backend/parser/Makefile
@@ -15,7 +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_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_relation.o parse_target.o parse_type.o parse_utilcmd.o scansup.o \
+      parse_temporal.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 0f7659b..60e4248 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"
@@ -1214,6 +1215,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");
@@ -1291,6 +1295,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 07cc81e..fda7f84 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -425,6 +425,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
@@ -476,11 +477,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
@@ -574,6 +578,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>		partbound_datum_list
 %type <partrange_datum>	PartitionRangeDatum
 %type <list>		range_datum_list
+%type <list>    temporal_bounds_list
+
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -598,7 +604,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
@@ -644,7 +650,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
 
@@ -11108,6 +11114,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.
@@ -11364,6 +11383,94 @@ 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 period boundaries with 4 comparable values
+					 * or two range typed values,
+					 * i.e. [lts, lte) is the left argument period, and
+					 *		[rts, rte) is the right argument period,
+					 * or two compatible range types with bounds like '[)'
+					 */
+					if(list_length($6) == 4 || list_length($6) == 2)
+						n->temporalBounds = $6;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have 2 range typed values, or four " \
+										"single 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 period boundaries with 4 comparable values
+					 * or two range typed values,
+					 * i.e. [lts, lte) is the left argument period, and
+					 *      [rts, rte) is the right argument period,
+					 * or two compatible range types with bounds like '[)'
+					 */
+					if(list_length($5) == 4 || list_length($5) == 2)
+						n->temporalBounds = $5;
+					else
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("Temporal adjustment boundaries must " \
+										"have 2 range typed values, or four " \
+										"single values."),
+								 parser_errposition(@5)));
+
+					$$ = n;
+				}
 		;
 
 
@@ -14681,7 +14788,8 @@ type_func_name_keyword:
  * forced to.
  */
 reserved_keyword:
-			  ALL
+			  ALIGN
+			| ALL
 			| ANALYSE
 			| ANALYZE
 			| AND
@@ -14729,6 +14837,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 69f4736..f65ae6a 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -39,6 +39,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"
@@ -941,6 +942,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.
 		 */
@@ -1003,6 +1041,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..1132274
--- /dev/null
+++ b/src/backend/parser/parse_temporal.c
@@ -0,0 +1,1668 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 can have four elements in a boundary
+ * list (i.e., WITH-clause of a temporal primitive) when we have two argument
+ * relations with scalar boundaries, or two entries if we have 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_LARGTST = 0,
+	TPB_LARGTE,
+	TPB_RARGTST,
+	TPB_RARGTE
+} 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,
+						  bool *hasRangeTypes,
+						  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,
+						   bool hasRangeTypes,
+						   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. These are either
+ * 		two scalar values for TS and TE, or a single range type value T holding
+ * 		both bounds.
+ */
+static List *
+temporalBoundGetLeftBounds(List *bounds)
+{
+	switch(list_length(bounds))
+	{
+		case 2: return list_make1(linitial(bounds));
+		case 4: return list_make2(linitial(bounds), lsecond(bounds));
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT),
+			 errmsg("Invalid temporal bound list length."),
+			 errhint("Specify four scalar columns for the " \
+					 "temporal boundaries, or two range-typed "\
+					 "columns.")));
+
+	/* Keep compiler quiet */
+	return NIL;
+}
+
+/*
+ * temporalBoundGetRightBounds -
+ * 		Return the right boundaries of a temporal bounds list. These are either
+ * 		two scalar values for TS and TE, or a single range type value T holding
+ * 		both bounds.
+ */
+static List *
+temporalBoundGetRightBounds(List *bounds)
+{
+	switch(list_length(bounds))
+	{
+		case 2: return list_make1(lsecond(bounds));
+		case 4: return list_make2(lthird(bounds), lfourth(bounds));
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_INVALID_ARGUMENT_FOR_TEMPORAL_ADJUSTMENT),
+			 errmsg("Invalid temporal bound list length."),
+			 errhint("Specify four scalar columns for the " \
+					 "temporal boundaries, or two range-typed "\
+					 "columns.")));
+
+	/* Keep compiler quiet */
+	return NIL;
+}
+
+/*
+ * 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)
+	{
+		/*
+		 * Four boundary entries means that we have 2x two scalar boundaries.
+		 * Which means the first two entries are start and end of the first
+		 * bound, and the 3th and 4th entry are start and end of the second
+		 * bound.
+		 */
+		case 4:
+			return list_nth(bounds, id);
+
+		/*
+		 * 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_LARGTST)
+				return linitial(bounds);
+			if(id == TPB_RARGTST || id == TPB_LARGTE)
+				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_LARGTST)
+				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 four scalar columns for the " \
+						 "temporal boundaries, or two range-typed "\
+						 "columns.")));
+
+	return NULL;
+}
+
+/*
+ * transformTemporalClause -
+ * 		If we have a temporal primitive query, we must find all attribute
+ * 		numbers for p1, p2, rn, ts, te, 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, all attribute numbers for temporal boundaries are unknown */
+	tc->attNumTr = -1;
+	tc->attNumTe = -1;
+	tc->attNumTs = -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;
+	}
+
+	/*
+	 * If we have range types the subquery splits it into separate
+	 * columns, called ts and te which are in between the p1- and
+	 * rn-column.
+	 */
+	if(tc->colnameTr)
+	{
+		tc->attNumTe = pos--;
+		tc->attNumTs = pos--;
+	}
+
+	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 if(strcmp(key, "ts") == 0)
+		{
+			tc->colnameTs = pstrdup(value);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumTs);
+			foundTsTe = true;
+		}
+		else if(strcmp(key, "te") == 0)
+		{
+			tc->colnameTe = pstrdup(value);
+			tle = get_tle_by_resno(qry->targetList, tc->attNumTe);
+			foundTsTe = true;
+		}
+		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;
+
+		/* Temporal boundary is a range type */
+		if (tc->colnameTr)
+		{
+			if (strcmp(tle->resname, tc->colnameTr) == 0)
+				tc->attNumTr = tle->resno;
+		}
+		else /* Two scalar columns for boundaries */
+		{
+			if (strcmp(tle->resname, tc->colnameTs) == 0)
+				tc->attNumTs = tle->resno;
+			else if (strcmp(tle->resname, tc->colnameTe) == 0)
+				tc->attNumTe = tle->resno;
+		}
+	}
+
+	/* We need column attribute numbers for all temporal boundaries */
+	if(tc->attNumTs == -1
+			|| tc->attNumTe == -1
+			|| (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;
+
+	/* We use range types, remove splitted columns, i.e. upper/lower bounds */
+	if(tc->colnameTr) {
+		get_tle_by_resno(qry->targetList, tc->attNumTs)->resjunk = true;
+		get_tle_by_resno(qry->targetList, tc->attNumTe)->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, bool *hasRangeTypes, 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;
+
+	/*
+	 * We can have 2 or 4 column references, i.e. if we have 4, the first two
+	 * form the left argument period [largTs, largTe), and the last two the
+	 * right argument period respectively. Otherwise, we have two range typed
+	 * values of the form '[)' where the first argument contains the boundaries
+	 * of the left-hand-side, and the second argument contains the boundaries
+	 * of the RHS respectively. The parser checked already if there was another
+	 * number of arguments (not equal to 2 or 4) given.
+	 */
+	*hasRangeTypes = list_length(j->temporalBounds) == 2;
+
+	/*
+	 * 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);
+	if(*hasRangeTypes)
+	{
+		/*
+		 * Hardcoded column names for ts and te. We handle ambiguous column
+		 * names during the transformation of temporal primitive clauses.
+		 */
+		ssResult->temporalClause->colnameTs = "ts";
+		ssResult->temporalClause->colnameTe = "te";
+		ssResult->temporalClause->colnameTr =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTST);
+	}
+	else
+	{
+		ssResult->temporalClause->colnameTs =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTST);
+		ssResult->temporalClause->colnameTe =
+				temporalBoundGetName(j->temporalBounds, TPB_LARGTE);
+		ssResult->temporalClause->colnameTr = NULL;
+	}
+
+	/*
+	 * 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;
+	joinExpr->inTmpPrimHasRangeT = *hasRangeTypes;
+
+	return ssResult;
+}
+
+/*
+ * transformTemporalAligner -
+ * 		transform a TEMPORAL ALIGN clause into standard SQL
+ *
+ * INPUT:
+ * 		(r ALIGN s ON q WITH (r.ts, r.te, s.ts, s.te)) c
+ *
+ * 		-- or --
+ *
+ * 		(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, r.ts, r.te, s.ts, and s.te
+ * 		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 can be defined as
+ * 		four scalars, or two half-open, i.e., [), range typed values.
+ *
+ * OUTPUT:
+ * 		(
+ * 		SELECT r.*, GREATEST(r.ts, s.ts) P1, LEAST(r.te, s.te) P2
+ *      FROM
+ *      (
+ *      	SELECT *, row_id() OVER () rn FROM r
+ *      ) r
+ *      LEFT OUTER JOIN
+ *      s
+ *      ON q AND r.ts < s.te AND r.te > s.ts
+ *      ORDER BY rn, P1, P2
+ *      ) c
+ *
+ *      -- or --
+ *
+ *      (
+ * 		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;
+
+	bool				 hasRangeTypes;
+	SelectStmt			*ssResult;
+	RangeSubselect 		*rssResult;
+	ResTarget			*rtGreatest;
+	ResTarget			*rtLeast;
+	ResTarget			*rtLowerLarg;
+	ResTarget			*rtUpperLarg;
+	ColumnRef 			*crLargTs;
+	ColumnRef 			*crRargTs;
+	ColumnRef 			*crLargTe;
+	ColumnRef 			*crRargTe;
+	MinMaxExpr			*mmeGreatest;
+	MinMaxExpr			*mmeLeast;
+	FuncCall			*fcLowerLarg;
+	FuncCall			*fcLowerRarg;
+	FuncCall			*fcUpperLarg;
+	FuncCall			*fcUpperRarg;
+	List				*mmeGreatestArgs;
+	List				*mmeLeastArgs;
+	List				*boundariesExpr;
+	JoinExpr			*joinExpr;
+	A_Expr				*lowerBoundExpr;
+	A_Expr				*upperBoundExpr;
+	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, &hasRangeTypes,
+										 &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.
+	 */
+	if (hasRangeTypes)
+	{
+		crLargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crRargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   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);
+
+		rtLowerLarg = makeResTarget((Node *) fcLowerLarg,
+									ssResult->temporalClause->colnameTs);
+		rtUpperLarg = makeResTarget((Node *) fcUpperLarg,
+									ssResult->temporalClause->colnameTe);
+
+		ssResult->targetList = list_concat(ssResult->targetList,
+										   list_make2(rtLowerLarg, rtUpperLarg));
+	}
+	else
+	{
+		crLargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crLargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTE,
+										   largAlias->aliasname);
+		crRargTs = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+		crRargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTE,
+										   rargAlias->aliasname);
+
+		/* Create argument list for function call to "greatest" and "least" */
+		mmeGreatestArgs = list_make2(crLargTs, crRargTs);
+		mmeLeastArgs = list_make2(crLargTe, crRargTe);
+
+		/*
+		 * Build Boolean expressions, i.e. "r.ts < s.te AND r.te > s.ts"
+		 * and concatenate it with q (=theta)
+		 */
+		lowerBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  "<",
+										  copyObject(crLargTs),
+										  copyObject(crRargTe),
+										  UNKNOWN_LOCATION);
+		upperBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  ">",
+										  copyObject(crLargTe),
+										  copyObject(crRargTs),
+										  UNKNOWN_LOCATION);
+
+		boundariesExpr = list_make2(lowerBoundExpr, upperBoundExpr);
+	}
+
+	/* 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.ts, r.te, s.ts, s.te)) c
+ *
+ * 		-- or --
+ *
+ * 		(r NORMALIZE s ON q WITH (r.t, s.t)) c
+ *
+ * 		-- or --
+ *
+ * 		(r NORMALIZE s USING(atts) WITH (r.ts, r.te, s.ts, s.te)) 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, s.t, r.ts, r.te, s.ts, and s.te
+ * 		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 can be defined as
+ * 		four scalars, or two half-open, i.e., [), range typed values.
+ *
+ * OUTPUT:
+ *
+ *		- The following lines show versions with four scalars as time
+ *		  point start, i.e., ts, and time point end, i.e., te.
+ *		- If range typed values, i.e., r.t and s.t, are used these
+ *		  changes to the code below apply:
+ *		  1) Replace: "P1 >= r.ts AND P1 < r.te" with "P1 <@ t"
+ *		  2) "ts P1" becomes "lower(s.t) P1"
+ *		  3) "te P1" becomes "upper(s.t) P1"
+ *
+ * 		(
+ * 			SELECT r.*,
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT s.*, ts P1 FROM s
+ *      		UNION ALL
+ *      		SELECT s.*, te P1 FROM s
+ *      	) s
+ *      	ON q AND P1 >= r.ts AND P1 < r.te
+ *      	ORDER BY rn, P1
+ *      ) c
+ *
+ *      -- or --
+ *
+ * 		(
+ * 			SELECT r.*,
+ *      	FROM
+ *      	(
+ *      		SELECT *, row_id() OVER () rn FROM r
+ *      	) r
+ *      	LEFT OUTER JOIN
+ *      	(
+ *      		SELECT atts, ts P1 FROM s
+ *      		UNION
+ *      		SELECT atts, te P1 FROM s
+ *      	) s
+ *      	ON r.atts = s.atts AND P1 >= r.ts AND P1 < r.te
+ *      	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;
+	ResTarget			*rtLowerLarg;
+	ResTarget			*rtUpperLarg;
+	ColumnRef 			*crRargStar;
+	ColumnRef 			*crLargTsT = NULL;
+	ColumnRef 			*crRargTsT = NULL;
+	ColumnRef 			*crLargTe = NULL;
+	ColumnRef 			*crRargTe = NULL;
+	ColumnRef 			*crP1;
+	JoinExpr			*joinExpr;
+	A_Expr				*lowerBoundExpr;
+	A_Expr				*upperBoundExpr;
+	A_Expr				*containsExpr;
+	Node				*boolExpr;
+	Alias				*largAlias;
+	Alias				*rargAlias;
+	char 				*colnameRN;
+	char 				*colnameP1;
+	bool				 hasRangeTypes;
+	FuncCall			*fcLowerLarg = NULL;
+	FuncCall			*fcUpperLarg = NULL;
+	FuncCall			*fcLowerRarg = NULL;
+	FuncCall			*fcUpperRarg = NULL;
+	List				*boundariesExpr;
+
+	/* Create a select statement skeleton to be filled here */
+	ssResult = makeTemporalQuerySkeleton(j, &colnameRN, &colnameP1,
+										 NULL, &hasRangeTypes,
+										 &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. If we need only two range types
+	 * only Ts columnrefs are used.
+	 */
+	if (hasRangeTypes)
+	{
+		crLargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crRargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+
+		/* Create argument list for function call to "lower" and "upper" */
+		fcLowerLarg = makeFuncCall(SystemFuncName("lower"),
+								   list_make1(crLargTsT),
+								   UNKNOWN_LOCATION);
+		fcLowerRarg = makeFuncCall(SystemFuncName("lower"),
+								   list_make1(crRargTsT),
+								   UNKNOWN_LOCATION);
+		fcUpperLarg = makeFuncCall(SystemFuncName("upper"),
+								   list_make1(crLargTsT),
+								   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);
+
+		rtLowerLarg = makeResTarget((Node *) fcLowerLarg,
+									ssResult->temporalClause->colnameTs);
+		rtUpperLarg = makeResTarget((Node *) fcUpperLarg,
+									ssResult->temporalClause->colnameTe);
+
+		ssResult->targetList = list_concat(ssResult->targetList,
+										   list_make2(rtLowerLarg, rtUpperLarg));
+		/*
+		 * 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);
+	}
+	else
+	{
+		/*
+		 * Build column references, for use later.
+		 */
+		crLargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTST,
+										   largAlias->aliasname);
+		crLargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_LARGTE,
+										   largAlias->aliasname);
+		crRargTsT = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTST,
+										   rargAlias->aliasname);
+		crRargTe = temporalBoundGetCopyFQN(j->temporalBounds, TPB_RARGTE,
+										   rargAlias->aliasname);
+
+		/* Build resource target "ts P1" and "te P1" */
+		rtTsP1 = makeResTarget((Node *) crRargTsT, colnameP1);
+		rtTeP1 = makeResTarget((Node *) crRargTe, colnameP1);
+		/*
+		 * Build "contains" expressions, i.e. "P1 >= ts AND P1 < te"
+		 * and concatenate it with q (=theta)
+		 */
+		lowerBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  ">=",
+										  copyObject(crP1),
+										  copyObject(crLargTsT),
+										  UNKNOWN_LOCATION);
+		upperBoundExpr = makeSimpleA_Expr(AEXPR_OP,
+										  "<",
+										  copyObject(crP1),
+										  copyObject(crLargTe),
+										  UNKNOWN_LOCATION);
+
+		boundariesExpr = list_make2(lowerBoundExpr, upperBoundExpr);
+	}
+
+	/*
+	 * Build "SELECT s.*, ts P1 FROM s" and "SELECT s.*, te P1 FROM s", iff we
+	 * have a ON-clause.
+	 * If we have an USING-clause with a name-list 'atts' build "SELECT atts,
+	 * ts P1 FROM s" and "SELECT atts, te 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	   *colnameTs = ssResult->temporalClause->colnameTs;
+		char	   *colnameTe = ssResult->temporalClause->colnameTe;
+		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(strcmp(usingItemName, colnameTs) == 0
+					|| strcmp(usingItemName, colnameTe) == 0
+					|| (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). These column
+ * 		names also differ depending on weither we have range types or scalars
+ * 		for temporal bounds. The list is then compared with the aliases from
+ * 		the current parser state, and renamed if necessary.
+ */
+static List *
+internalUseOnlyColumnNames(ParseState *pstate,
+						   bool hasRangeTypes,
+						   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"));
+
+	/* We split range types into upper and lower bounds, called ts and te */
+	if(hasRangeTypes)
+	{
+		filter = lappend(filter, makeString("ts"));
+		filter = lappend(filter, makeString("te"));
+	}
+
+	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. This means, if
+ * 		we have only one item in boundary list, all bounds must be range types.
+ * 		Otherwise, all bounds must be scalars.
+ */
+static void
+temporalBoundCheckIntegrity(ParseState *pstate,
+							 List *bounds,
+							 List *colnames,
+							 List *colvars,
+							 TemporalType tmpType)
+{
+	ListCell 	*lcNames;
+	ListCell 	*lcVars;
+	ListCell 	*lcBound;
+	ListCell 	*lcFilter;
+	bool 		 hasRangeTypes = list_length(bounds) == 1;
+	List		*filter = internalUseOnlyColumnNames(pstate,
+													 hasRangeTypes,
+													 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(hasRangeTypes && type->typtype != TYPTYPE_RANGE)
+					msg = "Invalid column type \"%s\" for the temporal bound " \
+						  "\"%s\". It must be a range type column.";
+
+				if(! hasRangeTypes && type->typtype == TYPTYPE_RANGE)
+					msg = "Invalid column type \"%s\" for the temporal bound " \
+						  "\"%s\". It must be a scalar type column (i.e., " \
+						  "not a range type).";
+
+				if (strlen(msg) > 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+							 errmsg(msg,
+									NameStr(type->typname),
+									NameListToString(crb->fields)),
+							 errhint("Specify four scalar columns for the " \
+									 "temporal boundaries, or 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;
+	bool 		foundTS = false;
+	bool 		foundTE = false;
+	int 		counterP1 = -1;
+	int 		counterP2 = -1;
+	int 		counterRN = -1;
+	int 		counterTS = -1;
+	int 		counterTE = -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);
+
+		if (j->inTmpPrimHasRangeT)
+		{
+			getColumnCounter(colname, "ts", &foundTS, &counterTS);
+			getColumnCounter(colname, "te", &foundTE, &counterTE);
+		}
+	}
+
+	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 (j->inTmpPrimHasRangeT)
+		{
+			getColumnCounter(colname, "ts", &foundTS, &counterTS);
+			getColumnCounter(colname, "te", &foundTE, &counterTE);
+		}
+	}
+
+	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;
+			A_Expr *e2;
+			List *qualArgs;
+			bool hasRangeTypes = list_length(j->temporalBounds) == 2;
+
+			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(! hasRangeTypes)
+			{
+				e2 = (A_Expr *) lsecond(qualArgs);
+				linitial(((ColumnRef *)e2->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);
+
+	if (foundTS)
+		addTemporalAlias(pstate, "ts", counterTS);
+
+	if (foundTE)
+		addTemporalAlias(pstate, "te", counterTE);
+
+	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;
+	j->inTmpPrimHasRangeT = list_length(bounds) == 2;
+
+	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;
+	j->inTmpPrimHasRangeT = list_length(bounds) == 2;
+
+	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 46aadd7..7516767 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -204,6 +204,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 bb7053a..a2cdd48 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -5000,6 +5000,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 = 3999 (  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 1c1cb80..62af9e2 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1291,6 +1291,30 @@ 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 */
+	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 1f4bad7..ac24f95 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 95dd8ba..b23dca2 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -80,6 +80,7 @@ typedef enum NodeTag
 	T_SetOp,
 	T_LockRows,
 	T_Limit,
+	T_TemporalAdjustment,
 	/* these aren't subclasses of Plan: */
 	T_NestLoopParam,
 	T_PlanRowMark,
@@ -129,6 +130,7 @@ typedef enum NodeTag
 	T_SetOpState,
 	T_LockRowsState,
 	T_LimitState,
+	T_TemporalAdjustmentState,
 
 	/*
 	 * TAGS FOR PRIMITIVE NODES (primnodes.h)
@@ -260,6 +262,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_PartitionSpec,
 	T_PartitionBoundSpec,
 	T_PartitionRangeDatum,
+	T_TemporalClause,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
@@ -690,7 +694,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 5afc3eb..9b4a6b9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -162,6 +162,8 @@ typedef struct Query
 										 * 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
@@ -1449,6 +1451,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 f72f7a8..8be446d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -220,6 +220,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 235bc75..763ad42 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -51,6 +51,35 @@ 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   attNumTs;
+	AttrNumber   attNumTe;
+	AttrNumber   attNumTr;
+	AttrNumber   attNumP1;
+	AttrNumber   attNumP2;
+	AttrNumber   attNumRN;
+
+	char        *colnameTs;
+	char        *colnameTe;
+	char		*colnameTr;	    /* If range type used for bounds, or NULL */
+} TemporalClause;
+
 /*
  * RangeVar - range variable, used in FROM clauses
  *
@@ -1416,6 +1445,10 @@ 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 */
+	bool		 inTmpPrimHasRangeT; /* true, if bounds are range types */
 } JoinExpr;
 
 /*----------
diff --git a/src/include/nodes/print.h b/src/include/nodes/print.h
index 4c94a3a..c6cbbf4 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 f7ac6f6..2b833fb 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1057,6 +1057,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 53cad24..7342037 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -154,6 +154,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 985d650..de89969 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)
@@ -257,6 +258,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 3a25d95..825c973 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -196,6 +196,12 @@ struct ParseState
 	bool		p_hasModifyingCTE;
 
 	/*
+	 * 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..6e4cc0d
--- /dev/null
+++ b/src/test/regress/expected/temporal_primitives.out
@@ -0,0 +1,841 @@
+--
+-- TEMPORAL PRIMITIVES
+--
+SET datestyle TO ymd;
+CREATE COLLATION "de_DE.utf8" (LC_COLLATE = "de_DE.utf8",
+                               LC_CTYPE = "de_DE.utf8" );
+CREATE TEMP TABLE tpg_table1 (a char, b char, ts int, te int);
+CREATE TEMP TABLE tpg_table2 (c int, d char, ts int, te int);
+INSERT INTO tpg_table1 VALUES
+('a','B',1,7),
+('b','B',3,9),
+('c','G',8,10);
+INSERT INTO tpg_table2 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 tpg_table3 AS
+	SELECT a, ts, te, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table4 AS
+	SELECT c, ts, d, te FROM tpg_table2;
+-- VALID TIME columns represented as range type
+CREATE TEMP TABLE tpg_table5 AS
+	SELECT int4range(ts, te) t, a, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table6 AS
+	SELECT int4range(ts, te) t, c a, d b FROM tpg_table2;
+-- VALID TIME columns as VARCHARs
+CREATE TEMP TABLE tpg_table7 (a int, ts varchar, te varchar);
+CREATE TEMP TABLE tpg_table8 (a int,
+							  ts varchar COLLATE "de_DE.utf8",
+							  te varchar COLLATE "POSIX");
+INSERT INTO tpg_table7 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X'),
+(0, 'ABC', 'BCD'),
+(0, 'xABC', 'xBCD'),
+(0, 'BAA', 'BBB');
+INSERT INTO tpg_table8 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X');
+-- Tables to check different data types, and corner cases
+CREATE TEMP TABLE tpg_table9 (a int, ts timestamp, te timestamp);
+CREATE TEMP TABLE tpg_table10 (a int, ts double precision, te double precision);
+CREATE TEMP TABLE tpg_table11 AS TABLE tpg_table10;
+INSERT INTO tpg_table9 VALUES
+(0, '2000-01-01', '2000-01-10'),
+(1, '2000-01-05', '2000-01-20');
+INSERT INTO tpg_table10 VALUES
+(0, 1.0, 1.1111),
+(1, 1.11109999, 2.0);
+INSERT INTO tpg_table11 VALUES
+(0, 1.0, 'Infinity'),
+(1, '-Infinity', 2.0);
+--
+-- TEMPORAL ALIGNER: BASICS
+--
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 ALIGN tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | ts | te | 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) x;
+ a | ts | te | 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table1 ALIGN tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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)
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 ALIGN tpg_table6
+		ON TRUE
+		WITH (t, t)
+	) x;
+   t    | a | b 
+--------+---+---
+ [1,2)  | a | B
+ [2,5)  | a | B
+ [3,4)  | a | B
+ [5,7)  | a | B
+ [3,4)  | b | B
+ [3,5)  | b | B
+ [5,7)  | b | B
+ [7,9)  | b | B
+ [8,9)  | c | G
+ [9,10) | c | G
+(10 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 tpg_table5),
+	 t2 AS (SELECT *, t v FROM tpg_table6)
+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)
+
+-- Full temporal join example with absorbing where clause, timestamp
+-- propagation (see CTEs targetlists with V and U) and scalar VALID TIME columns
+WITH t1 AS (SELECT *, ts us, te ue FROM tpg_table1),
+	 t2 AS (SELECT *, ts vs, te ve FROM tpg_table2)
+SELECT x.ts, x.te, b, a, c FROM (
+	t1 ALIGN t2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	LEFT OUTER JOIN (
+		SELECT * FROM (
+		t2 ALIGN t1
+			ON b = d
+			WITH (ts, te, ts, te)
+		) y
+	) y
+	ON b = d AND x.ts = y.ts AND x.te = y.te
+	WHERE (
+			(x.ts = us OR x.ts = vs)
+			AND
+			(x.te = ue OR x.te = ve)
+		)
+		OR us IS NULL
+		OR vs IS NULL
+	ORDER BY 1,2,3,4;
+ ts | te | b | a | c 
+----+----+---+---+---
+  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 (
+	tpg_table7 x ALIGN tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ a |  ts  |  te  
+---+------+------
+ 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)
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x ALIGN tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ERROR:  could not determine which collation to use for string comparison
+HINT:  Use the COLLATE clause to set the collation explicitly.
+--
+-- 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 (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+                                                          QUERY PLAN                                                          
+------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(tpg_table2.ts, tpg_table1.ts)), (LEAST(tpg_table2.te, tpg_table1.te))
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table2.ts < tpg_table1.te) AND (tpg_table2.te > tpg_table1.ts))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Seq Scan on tpg_table1
+(11 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+                                                          QUERY PLAN                                                          
+------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: (x.ts > 3)
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(tpg_table2.ts, tpg_table1.ts)), (LEAST(tpg_table2.te, tpg_table1.te))
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table2.ts < tpg_table1.te) AND (tpg_table2.te > tpg_table1.ts))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Seq Scan on tpg_table1
+(12 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 3;
+                                                          QUERY PLAN                                                          
+------------------------------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: ((x.c < 3) OR (x.ts > 3))
+   ->  Adjustment(for ALIGN)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), (GREATEST(tpg_table2.ts, tpg_table1.ts)), (LEAST(tpg_table2.te, tpg_table1.te))
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table2.ts < tpg_table1.te) AND (tpg_table2.te > tpg_table1.ts))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                     ->  Materialize
+                           ->  Seq Scan on tpg_table1
+(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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 ALIGN tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) 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, ts, te FROM (
+	tpg_table10 t1 ALIGN tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |     ts     |   te   
+---+------------+--------
+ 0 |          1 | 1.1111
+ 0 | 1.11109999 | 1.1111
+ 1 | 1.11109999 |      2
+(3 rows)
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 ALIGN tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |    ts     |    te    
+---+-----------+----------
+ 0 |         1 |        2
+ 0 |         1 | Infinity
+ 1 | -Infinity |        2
+(3 rows)
+
+--
+-- TEMPORAL NORMALIZER: BASICS
+--
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 NORMALIZE tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+ a | ts | te | 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 ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT * FROM (
+	tpg_table3 NORMALIZE tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) x;
+ a | ts | te | 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 ts and te are not the last two columns.
+-- Please note: This was a restriction in an early implementation.
+SELECT a, COUNT(a) FROM (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table1 NORMALIZE tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 t1 NORMALIZE tpg_table1 t2
+		USING (a)
+		WITH (ts, te, ts, te)
+	) x;
+ a | b | ts | te 
+---+---+----+----
+ a | B |  1 |  7
+ b | B |  3 |  9
+ c | G |  8 | 10
+(3 rows)
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 NORMALIZE tpg_table6
+		USING (b)
+		WITH (t, t)
+	) x;
+   t    | a | b 
+--------+---+---
+ [1,2)  | a | B
+ [2,3)  | a | B
+ [3,4)  | a | B
+ [4,5)  | a | B
+ [5,7)  | a | B
+ [3,4)  | b | B
+ [4,5)  | b | B
+ [5,7)  | b | B
+ [7,9)  | b | B
+ [8,10) | c | G
+(10 rows)
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	tpg_table7 x NORMALIZE tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ a |  ts  |  te  
+---+------+------
+ 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)
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x NORMALIZE tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+ERROR:  could not determine which collation to use for string comparison
+HINT:  Use the COLLATE clause to set the collation explicitly.
+--
+-- 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 (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+                                               QUERY PLAN                                                
+---------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), tpg_table1.ts
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table1.ts >= tpg_table2.ts) AND (tpg_table1.ts < tpg_table2.te))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on tpg_table1
+                                 ->  Seq Scan on tpg_table1 tpg_table1_1
+(13 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+                                               QUERY PLAN                                                
+---------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: (x.ts > 3)
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), tpg_table1.ts
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table1.ts >= tpg_table2.ts) AND (tpg_table1.ts < tpg_table2.te))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                                 Filter: (c < 3)
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on tpg_table1
+                                 ->  Seq Scan on tpg_table1 tpg_table1_1
+(14 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 3;
+                                               QUERY PLAN                                                
+---------------------------------------------------------------------------------------------------------
+ Subquery Scan on x
+   Filter: ((x.c < 3) OR (x.ts > 3))
+   ->  Adjustment(for NORMALIZE)
+         ->  Sort
+               Sort Key: (row_id() OVER (?)), tpg_table1.ts
+               ->  Nested Loop Left Join
+                     Join Filter: ((tpg_table1.ts >= tpg_table2.ts) AND (tpg_table1.ts < tpg_table2.te))
+                     ->  WindowAgg
+                           ->  Seq Scan on tpg_table2
+                     ->  Materialize
+                           ->  Append
+                                 ->  Seq Scan on tpg_table1
+                                 ->  Seq Scan on tpg_table1 tpg_table1_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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 NORMALIZE tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) 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, ts, te FROM (
+	tpg_table10 t1 NORMALIZE tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |     ts     |     te     
+---+------------+------------
+ 0 |          1 | 1.11109999
+ 0 | 1.11109999 |     1.1111
+ 1 | 1.11109999 |          2
+(3 rows)
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 NORMALIZE tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+ a |    ts     |    te    
+---+-----------+----------
+ 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 (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+TABLE v;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+TABLE v;
+ a | b | ts | te 
+---+---+----+----
+ 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) NORMALIZE tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1", p1_1, ts, te)
+	) x;
+TABLE v;
+ p1 | p1_0 | p1_-1 | 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) ALIGN tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1",p1_1,ts,te)
+	) x;
+TABLE v;
+ p1 | p1_0 | p1_-1 | 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 edeb2d6..0dc8ad1 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
+test: alter_generic alter_operator misc psql async dbsize misc_functions sysviews tsrf 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..9b475f7
--- /dev/null
+++ b/src/test/regress/sql/temporal_primitives.sql
@@ -0,0 +1,456 @@
+--
+-- TEMPORAL PRIMITIVES
+--
+SET datestyle TO ymd;
+
+CREATE COLLATION "de_DE.utf8" (LC_COLLATE = "de_DE.utf8",
+                               LC_CTYPE = "de_DE.utf8" );
+
+CREATE TEMP TABLE tpg_table1 (a char, b char, ts int, te int);
+CREATE TEMP TABLE tpg_table2 (c int, d char, ts int, te int);
+
+INSERT INTO tpg_table1 VALUES
+('a','B',1,7),
+('b','B',3,9),
+('c','G',8,10);
+INSERT INTO tpg_table2 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 tpg_table3 AS
+	SELECT a, ts, te, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table4 AS
+	SELECT c, ts, d, te FROM tpg_table2;
+
+-- VALID TIME columns represented as range type
+CREATE TEMP TABLE tpg_table5 AS
+	SELECT int4range(ts, te) t, a, b FROM tpg_table1;
+CREATE TEMP TABLE tpg_table6 AS
+	SELECT int4range(ts, te) t, c a, d b FROM tpg_table2;
+
+-- VALID TIME columns as VARCHARs
+CREATE TEMP TABLE tpg_table7 (a int, ts varchar, te varchar);
+CREATE TEMP TABLE tpg_table8 (a int,
+							  ts varchar COLLATE "de_DE.utf8",
+							  te varchar COLLATE "POSIX");
+
+INSERT INTO tpg_table7 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X'),
+(0, 'ABC', 'BCD'),
+(0, 'xABC', 'xBCD'),
+(0, 'BAA', 'BBB');
+
+INSERT INTO tpg_table8 VALUES
+(0, 'A', 'D'),
+(1, 'C', 'X');
+
+-- Tables to check different data types, and corner cases
+CREATE TEMP TABLE tpg_table9 (a int, ts timestamp, te timestamp);
+CREATE TEMP TABLE tpg_table10 (a int, ts double precision, te double precision);
+CREATE TEMP TABLE tpg_table11 AS TABLE tpg_table10;
+
+INSERT INTO tpg_table9 VALUES
+(0, '2000-01-01', '2000-01-10'),
+(1, '2000-01-05', '2000-01-20');
+
+INSERT INTO tpg_table10 VALUES
+(0, 1.0, 1.1111),
+(1, 1.11109999, 2.0);
+
+INSERT INTO tpg_table11 VALUES
+(0, 1.0, 'Infinity'),
+(1, '-Infinity', 2.0);
+
+
+--
+-- TEMPORAL ALIGNER: BASICS
+--
+
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+
+-- Alignment with aggregation
+-- NB: Targetlist of outer query is *not* A_STAR...
+SELECT a, COUNT(a) FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) 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 (
+	tpg_table3 ALIGN tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	GROUP BY a ORDER BY a;
+
+-- Test relations with differently named temporal bound attributes and relation
+-- and column aliases.
+SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 ALIGN tpg_table6
+		ON TRUE
+		WITH (t, t)
+	) 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 tpg_table5),
+	 t2 AS (SELECT *, t v FROM tpg_table6)
+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;
+
+-- Full temporal join example with absorbing where clause, timestamp
+-- propagation (see CTEs targetlists with V and U) and scalar VALID TIME columns
+WITH t1 AS (SELECT *, ts us, te ue FROM tpg_table1),
+	 t2 AS (SELECT *, ts vs, te ve FROM tpg_table2)
+SELECT x.ts, x.te, b, a, c FROM (
+	t1 ALIGN t2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	LEFT OUTER JOIN (
+		SELECT * FROM (
+		t2 ALIGN t1
+			ON b = d
+			WITH (ts, te, ts, te)
+		) y
+	) y
+	ON b = d AND x.ts = y.ts AND x.te = y.te
+	WHERE (
+			(x.ts = us OR x.ts = vs)
+			AND
+			(x.te = ue OR x.te = ve)
+		)
+		OR us IS NULL
+		OR vs IS NULL
+	ORDER BY 1,2,3,4;
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	tpg_table7 x ALIGN tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x ALIGN tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 ALIGN tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 ALIGN tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision
+SELECT a, ts, te FROM (
+	tpg_table10 t1 ALIGN tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 ALIGN tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+
+--
+-- TEMPORAL NORMALIZER: BASICS
+--
+
+-- Equality qualifiers
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Equality qualifiers with FQN inside ON- and WITH-clause
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON tpg_table1.b = tpg_table2.d
+		WITH (tpg_table1.ts, tpg_table1.te, tpg_table2.ts, tpg_table2.te)
+	) x;
+
+-- Alignment with aggregation
+-- NB: Targetlist of outer query is *not* A_STAR...
+SELECT a, COUNT(a) FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON tpg_table3.b = tpg_table4.d
+		WITH (tpg_table3.ts, tpg_table3.te, tpg_table4.ts, tpg_table4.te)
+	) 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 (
+	tpg_table3 NORMALIZE tpg_table4
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x
+	GROUP BY a ORDER BY a;
+
+-- Test relations with differently named temporal bound attributes and relation
+-- and column aliases.
+SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2 x(c,d,s,e)
+		ON b = d
+		WITH (ts, te, s, e)
+	) x;
+
+-- Normalizer's USING clause (self-normalization)
+SELECT * FROM (
+	tpg_table1 t1 NORMALIZE tpg_table1 t2
+		USING (a)
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Range types for temporal boundaries, i.e., valid time columns
+SELECT * FROM (
+	tpg_table5 NORMALIZE tpg_table6
+		USING (b)
+		WITH (t, t)
+	) x;
+
+-- Collation and varchar boundaries
+SELECT * FROM (
+	tpg_table7 x NORMALIZE tpg_table7 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Collation and varchar boundaries with incompatible collations (ERROR expected)
+SELECT * FROM (
+	tpg_table8 x NORMALIZE tpg_table8 y
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 AND ts > 3;
+
+EXPLAIN (COSTS OFF) SELECT * FROM (
+	tpg_table2 NORMALIZE tpg_table1
+		ON TRUE
+		WITH (ts, te, ts, te)
+	) x
+	WHERE c < 3 OR ts > 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(ts, 'YYYY-MM-DD') ts, to_char(te, 'YYYY-MM-DD') te FROM (
+	tpg_table9 t1 NORMALIZE tpg_table9 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision
+SELECT a, ts, te FROM (
+	tpg_table10 t1 NORMALIZE tpg_table10 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+-- Data types: Double precision with +/- infinity
+SELECT a, ts, te FROM (
+	tpg_table11 t1 NORMALIZE tpg_table11 t2
+		ON t1.a = 0
+		WITH (ts, te, ts, te)
+	) x;
+
+--
+-- TEMPORAL ALIGNER AND NORMALIZER: VIEWS
+--
+
+-- Views with temporal normalization
+CREATE TEMP VIEW v AS SELECT * FROM (
+	tpg_table1 NORMALIZE tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) x;
+
+TABLE v;
+DROP VIEW v;
+
+-- Views with temporal alignment
+CREATE TEMP VIEW v AS SELECT * FROM (
+	tpg_table1 ALIGN tpg_table2
+		ON b = d
+		WITH (ts, te, ts, te)
+	) 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) NORMALIZE tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1", p1_1, ts, te)
+	) 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 (
+	tpg_table1 AS r(p1, p1_0, "p1_-1", p1_1) ALIGN tpg_table2 s
+		ON r.p1_0 = s.d
+		WITH ("p1_-1",p1_1,ts,te)
+	) x;
+
+TABLE v;
+DROP VIEW v;
+
+
#23Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Robert Haas (#20)
Re: [PROPOSAL] Temporal query processing with range types

On 2/16/17 07:41, Robert Haas wrote:

Also, it sounds like all of this is intended to work with ranges that
are stored in different columns rather than with PostgreSQL's built-in
range types.

Yeah, that should certainly be changed.

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#24Peter Moser
pitiz29a@gmail.com
In reply to: Peter Eisentraut (#23)
Re: [PROPOSAL] Temporal query processing with range types

2017-02-22 19:43 GMT+01:00 Peter Eisentraut <peter.eisentraut@2ndquadrant.com>:

On 2/16/17 07:41, Robert Haas wrote:

Also, it sounds like all of this is intended to work with ranges that
are stored in different columns rather than with PostgreSQL's built-in
range types.

Yeah, that should certainly be changed.

Our syntax supports PostgreSQL's built-in range types and ranges that
are stored in different columns.

For instance, for range types an ALIGN query would look like this:
SELECT * FROM (r ALIGN s ON q WITH (r.t, s.t)) c

... and for ranges in different columns like this:
SELECT * FROM (r ALIGN s ON q WITH (r.ts, r.te, s.ts, s.te)) c

... where r and s are input relations, q can be any join qualifier, and
r.t, s.t, r.ts, r.te, s.ts, and s.te 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 can
be defined as four scalars, or two half-open, i.e., [), range typed
values.

It would reduce the size of our patch and simplify the overall structure,
if we would remove the possibility to express valid time start points and end
points in different columns.

Do you think it is better to remove the syntax for ranges expressed in
different columns?

However, internally we still need to split the
range types into two separate points, because NORMALIZE does 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 (see executor/nodeTemporalAdjustment.c). A second
constraint is, that we support currently only half-open intervals, that is,
any interval definition (open/closed/half-open) different from the PostgreSQL's
default, i.e., [), leads to undefined results.

Best regards,
Anton, Johann, Michael, Peter

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#25Jim Nasby
Jim.Nasby@BlueTreble.com
In reply to: Peter Moser (#24)
Re: [PROPOSAL] Temporal query processing with range types

On 2/24/17 6:40 AM, Peter Moser wrote:

Do you think it is better to remove the syntax for ranges expressed in
different columns?

It's not that hard to construct a range type on-the-fly from 2 columns,
so (without having looked at the patch or really followed the thread) I
would think the answer is yes.
--
Jim Nasby, Data Architect, Blue Treble Consulting, Austin TX
Experts in Analytics, Data Architecture and PostgreSQL
Data in Trouble? Get it in Treble! http://BlueTreble.com
855-TREBLE2 (855-873-2532)

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#26Peter Moser
pitiz29a@gmail.com
In reply to: Jim Nasby (#25)
Re: [PROPOSAL] Temporal query processing with range types

2017-02-24 21:25 GMT+01:00 Jim Nasby <Jim.Nasby@bluetreble.com>:

On 2/24/17 6:40 AM, Peter Moser wrote:

Do you think it is better to remove the syntax for ranges expressed in
different columns?

It's not that hard to construct a range type on-the-fly from 2 columns, so
(without having looked at the patch or really followed the thread) I would
think the answer is yes.

We discussed and decided to remove the syntax for separate columns.
The patch with "range-types-only" syntax will be send soon.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#27Peter Moser
pitiz29a@gmail.com
In reply to: Peter Moser (#26)
1 attachment(s)
Re: [PROPOSAL] Temporal query processing with range types

2017-02-27 11:21 GMT+01:00 Peter Moser <pitiz29a@gmail.com>:

2017-02-24 21:25 GMT+01:00 Jim Nasby <Jim.Nasby@bluetreble.com>:

It's not that hard to construct a range type on-the-fly from 2 columns, so
(without having looked at the patch or really followed the thread) I would
think the answer is yes.

Thank you for your suggestion.

We discussed and decided to remove the syntax for separate columns.

Please find attached the new patch with "range-type-only" syntax. It
is around 400 lines of
code shorter than its predecessor.

Now the syntax is as follows:

SELECT * FROM ( r ALIGN s ON q WITH (r.time, s.time) ) c;
SELECT * FROM ( r NORMALIZE s ON q WITH (r.time, s.time) ) c;
SELECT * FROM ( r NORMALIZE s USING(atts) WITH (r.time, s.time) ) c;

...where r and s are relations, c an alias, q any boolean expression,
atts a column name list, and r.time/s.time range typed columns.

This means that the syntax with four columns (i.e., scalar time point
start/end for relations r and s)
inside the WITH-clause is no longer supported.

Best regards,
Anton, Michael, Johann, Peter

Attachments:

tpg_primitives_out_v6.patchtext/x-patch; charset=US-ASCII; name=tpg_primitives_out_v6.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c9e0a3e..f7056e9 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -899,6 +899,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 2a2b7eb..490a1c6 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -26,6 +26,8 @@ OBJS = execAmi.o execCurrent.o execGrouping.o execIndexing.o execJunk.o \
        nodeSamplescan.o nodeSeqscan.o nodeSetOp.o nodeSort.o nodeUnique.o \
        nodeValuesscan.o nodeCtescan.o nodeWorktablescan.o \
        nodeGroup.o nodeSubplan.o nodeSubqueryscan.o nodeTidscan.o \
-       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.o
+       nodeForeignscan.o nodeWindowAgg.o tstoreReceiver.o tqueue.o spi.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 ef6f35a..7ecbe14 100644
--- a/src/backend/executor/execProcnode.c
+++ b/src/backend/executor/execProcnode.c
@@ -115,6 +115,7 @@
 #include "executor/nodeValuesscan.h"
 #include "executor/nodeWindowAgg.h"
 #include "executor/nodeWorktablescan.h"
+#include "executor/nodeTemporalAdjustment.h"
 #include "nodes/nodeFuncs.h"
 #include "miscadmin.h"
 
@@ -340,6 +341,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 */
@@ -541,6 +547,10 @@ ExecProcNode(PlanState *node)
 			result = ExecLimit((LimitState *) node);
 			break;
 
+		case T_TemporalAdjustmentState:
+			result = ExecTemporalAdjustment((TemporalAdjustmentState *) node);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
 			result = NULL;
@@ -793,6 +803,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;
@@ -834,3 +848,4 @@ ExecShutdownNode(PlanState *node)
 
 	return false;
 }
+
diff --git a/src/backend/executor/nodeTemporalAdjustment.c b/src/backend/executor/nodeTemporalAdjustment.c
new file mode 100644
index 0000000..fe65564
--- /dev/null
+++ b/src/backend/executor/nodeTemporalAdjustment.c
@@ -0,0 +1,570 @@
+#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;
+
+	/*
+	 * 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 05d8538..f0b52ab 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1968,6 +1968,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;
 }
@@ -2329,6 +2331,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)
 {
@@ -2788,6 +2825,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);
 
@@ -2869,6 +2907,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);
@@ -5286,6 +5325,12 @@ copyObject(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 d595cd7..1618a5f 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -756,6 +756,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 7586cce..0a2297f 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 -
@@ -610,3 +693,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 b3802b4..04c418b 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1559,6 +1559,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
@@ -1837,6 +1839,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");
@@ -2525,6 +2539,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);
@@ -2601,6 +2616,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");
@@ -2732,6 +2775,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);
@@ -3707,6 +3751,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;
@@ -3804,6 +3851,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);
diff --git a/src/backend/nodes/print.c b/src/backend/nodes/print.c
index 926f226..e007947 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"
 
 
 /*
@@ -492,3 +493,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 d2f69fe..7969ac8 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.
@@ -460,6 +479,17 @@ _readRangeVar(void)
 	READ_DONE();
 }
 
+static ColumnRef *
+_readColumnRef(void)
+{
+	READ_LOCALS(ColumnRef);
+
+	READ_NODE_FIELD(fields);
+	READ_LOCATION_FIELD(location);
+
+	READ_DONE();
+}
+
 static IntoClause *
 _readIntoClause(void)
 {
@@ -1241,6 +1271,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();
 }
@@ -2458,6 +2490,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 633b5c1..3a3a41e 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 */
@@ -127,6 +128,7 @@ static void subquery_push_qual(Query *subquery,
 static void recurse_push_qual(Node *setOp, Query *topquery,
 				  RangeTblEntry *rte, Index rti, Node *qual);
 static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
+static bool allWindowFuncsHaveRowId(List *targetList);
 
 
 /*
@@ -2355,7 +2357,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;
 
 	/*
@@ -2465,6 +2468,7 @@ static void
 check_output_expressions(Query *subquery, pushdown_safety_info *safetyInfo)
 {
 	ListCell   *lc;
+	bool wfsafe = allWindowFuncsHaveRowId(subquery->targetList);
 
 	foreach(lc, subquery->targetList)
 	{
@@ -2503,12 +2507,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;
+			}
+		}
 	}
 }
 
@@ -2578,6 +2599,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
@@ -2821,6 +2868,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 1e953b4..ee8adc3 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,
@@ -272,6 +275,9 @@ static ModifyTable *make_modifytable(PlannerInfo *root,
 				 List *resultRelations, List *subplans,
 				 List *withCheckOptionLists, List *returningLists,
 				 List *rowMarks, OnConflictExpr *onconflict, int epqParam);
+static TemporalAdjustment *make_temporalAdjustment(Plan *lefttree,
+						TemporalClause *temporalClause,
+						List *sortClause);
 
 
 /*
@@ -469,6 +475,11 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
 											  (LimitPath *) best_path,
 											  flags);
 			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);
@@ -2276,6 +2287,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;
+}
+
 
 /*****************************************************************************
  *
@@ -4888,6 +4926,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 = 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 ca0ae78..733c88a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1972,6 +1972,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
@@ -4287,7 +4301,6 @@ create_ordered_paths(PlannerInfo *root,
 	return ordered_rel;
 }
 
-
 /*
  * make_group_input_target
  *	  Generate appropriate PathTarget for initial input to grouping nodes.
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 07ddbcf..39286de 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -610,6 +610,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 3eb2bb7..242fb72 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -2696,6 +2696,7 @@ finalize_plan(PlannerInfo *root, Plan *plan, Bitmapset *valid_params,
 		case T_Gather:
 		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 3248296..a5479c0 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2425,6 +2425,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 df9a9fb..2d64e48 100644
--- a/src/backend/parser/Makefile
+++ b/src/backend/parser/Makefile
@@ -15,7 +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_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_relation.o parse_target.o parse_type.o parse_utilcmd.o scansup.o \
+      parse_temporal.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 796b5c9..700df92 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"
@@ -1212,6 +1213,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");
@@ -1289,6 +1293,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 e833b2e..182e85a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -425,6 +425,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
@@ -476,11 +477,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
@@ -574,6 +578,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>		partbound_datum_list
 %type <partrange_datum>	PartitionRangeDatum
 %type <list>		range_datum_list
+%type <list>    temporal_bounds_list
+
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -598,7 +604,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
@@ -644,7 +650,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
 
@@ -11096,6 +11102,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.
@@ -11352,6 +11371,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;
+				}
 		;
 
 
@@ -14668,7 +14769,8 @@ type_func_name_keyword:
  * forced to.
  */
 reserved_keyword:
-			  ALL
+			  ALIGN
+			| ALL
 			| ANALYSE
 			| ANALYZE
 			| AND
@@ -14716,6 +14818,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 b5eae56..7323872 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -39,6 +39,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"
@@ -941,6 +942,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.
 		 */
@@ -1003,6 +1041,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 46aadd7..7516767 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -204,6 +204,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 a4cc86d..0f7b2b5 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -5000,6 +5000,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 = 3999 (  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 6332ea0..eb0efcc 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1292,6 +1292,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 1f4bad7..ac24f95 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 28aca92..add6f89 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -80,6 +80,7 @@ typedef enum NodeTag
 	T_SetOp,
 	T_LockRows,
 	T_Limit,
+	T_TemporalAdjustment,
 	/* these aren't subclasses of Plan: */
 	T_NestLoopParam,
 	T_PlanRowMark,
@@ -129,6 +130,7 @@ typedef enum NodeTag
 	T_SetOpState,
 	T_LockRowsState,
 	T_LimitState,
+	T_TemporalAdjustmentState,
 
 	/*
 	 * TAGS FOR PRIMITIVE NODES (primnodes.h)
@@ -260,6 +262,7 @@ typedef enum NodeTag
 	T_LockRowsPath,
 	T_ModifyTablePath,
 	T_LimitPath,
+	T_TemporalAdjustmentPath,
 	/* these aren't subclasses of Path: */
 	T_EquivalenceClass,
 	T_EquivalenceMember,
@@ -469,6 +472,7 @@ typedef enum NodeTag
 	T_PartitionSpec,
 	T_PartitionBoundSpec,
 	T_PartitionRangeDatum,
+	T_TemporalClause,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
@@ -691,7 +695,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 5afc3eb..9b4a6b9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -162,6 +162,8 @@ typedef struct Query
 										 * 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
@@ -1449,6 +1451,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 f72f7a8..8be446d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -220,6 +220,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 235bc75..3cca43d 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -51,6 +51,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
  *
@@ -1416,6 +1441,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 4c94a3a..c6cbbf4 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 f7ac6f6..2b833fb 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1057,6 +1057,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 53cad24..7342037 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -154,6 +154,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 985d650..de89969 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)
@@ -257,6 +258,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 3a25d95..825c973 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -196,6 +196,12 @@ struct ParseState
 	bool		p_hasModifyingCTE;
 
 	/*
+	 * 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 edeb2d6..0dc8ad1 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
+test: alter_generic alter_operator misc psql async dbsize misc_functions sysviews tsrf 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;
+
+
#28Peter Moser
pitiz29a@gmail.com
In reply to: Peter Moser (#27)
1 attachment(s)
Re: [PROPOSAL] Temporal query processing with range types

2017-02-15 20:24 GMT+01:00 Robert Haas <robertmhaas@gmail.com>:

There's no documentation in this patch. I'm not sure you want to go
to the trouble of writing SGML documentation until this has been
reviewed enough that it has a real chance of getting committed, but on
the other hand we're obviously all struggling to understand what it
does, so I think if not SGML documentation it at least needs a real
clear explanation of what the syntax is and does in a README or
something, even just for initial review.

The attached README explains the NORMALIZE operation step-by-step with
an example. That is, we start from a query input, show how we rewrite
it during parser stage, and show how the final execution generates
result tuples. A similar walkthrough for ALIGN will follow soon.

We are thankful for any suggestion or ideas, to be used to write a
good SGML documentation.

Best regards,
Anton, Michael, Johann, Peter

Attachments:

README-NORMALIZE.txttext/plain; charset=US-ASCII; name=README-NORMALIZE.txtDownload
#29Peter Moser
pitiz29a@gmail.com
In reply to: Peter Moser (#28)
1 attachment(s)
Re: [PROPOSAL] Temporal query processing with range types

2017-03-01 10:56 GMT+01:00 Peter Moser <pitiz29a@gmail.com>:

A similar walkthrough for ALIGN will follow soon.

We are thankful for any suggestion or ideas, to be used to write a
good SGML documentation.

The attached README explains the ALIGN operation step-by-step with a
TEMPORAL LEFT OUTER JOIN example. That is, we start from a query
input, show how we rewrite it during parser stage, and show how the
final execution generates result tuples.

Best regards,
Anton, Michael, Johann, Peter

Attachments:

README-ALIGN.txttext/plain; charset=US-ASCII; name=README-ALIGN.txtDownload
#30Andres Freund
andres@anarazel.de
In reply to: Peter Moser (#29)
Re: [PROPOSAL] Temporal query processing with range types

Hi,

On 2017-03-30 14:11:28 +0200, Peter Moser wrote:

2017-03-01 10:56 GMT+01:00 Peter Moser <pitiz29a@gmail.com>:

A similar walkthrough for ALIGN will follow soon.

We are thankful for any suggestion or ideas, to be used to write a
good SGML documentation.

The attached README explains the ALIGN operation step-by-step with a
TEMPORAL LEFT OUTER JOIN example. That is, we start from a query
input, show how we rewrite it during parser stage, and show how the
final execution generates result tuples.

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.

- Andres

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#31Peter Moser
pitiz29a@gmail.com
In reply to: Andres Freund (#30)
1 attachment(s)
Re: [PROPOSAL] Temporal query processing with range types

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

Attachments:

tpg_primitives_out_v7.patchtext/x-patch; name=tpg_primitives_out_v7.patchDownload
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;
+
+
#32Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Peter Moser (#31)
Re: [PROPOSAL] Temporal query processing with range types

On Tue, Aug 1, 2017 at 12:53 AM, Peter Moser <pitiz29a@gmail.com> wrote:

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

Hi Peter,

This patch still applies, but no longer compiles:

nodeTemporalAdjustment.c: In function ‘ExecTemporalAdjustment’:
nodeTemporalAdjustment.c:286:21: error: incompatible types when
assigning to type ‘Form_pg_attribute’ from type
‘FormData_pg_attribute’
node->datumFormat = curr->tts_tupleDescriptor->attrs[tc->attNumP1 - 1];
^

After commits 2cd70845 and c6293249 you need to change expressions of
that format to, for example:

node->datumFormat = TupleDescAttr(curr->tts_tupleDescriptor,
tc->attNumP1 - 1);

Thanks!

--
Thomas Munro
http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#33Simon Riggs
simon@2ndquadrant.com
In reply to: Peter Moser (#29)
Re: [PROPOSAL] Temporal query processing with range types

On 30 March 2017 at 13:11, Peter Moser <pitiz29a@gmail.com> wrote:

2017-03-01 10:56 GMT+01:00 Peter Moser <pitiz29a@gmail.com>:

A similar walkthrough for ALIGN will follow soon.

We are thankful for any suggestion or ideas, to be used to write a
good SGML documentation.

The attached README explains the ALIGN operation step-by-step with a
TEMPORAL LEFT OUTER JOIN example. That is, we start from a query
input, show how we rewrite it during parser stage, and show how the
final execution generates result tuples.

Thanks for this contribution. I know what it takes to do significant
contributions and I know it can be frustrating when things languish
for months.

I am starting to look at temporal queries myself so I will begin an interest.

PostgreSQL tries really very hard to implement the SQL Standard and
just the standard. ISTM that the feedback you should have been given
is that this is very interesting but will not be committed in its
current form; I am surprised to see nobody has said that, though you
can see the truth of that since nobody is actively looking to review
or commit this. Obviously if the standard were changed to support
these things we'd suddenly be interested...

What I think I'm lacking is a clear statement of why we need to have
new syntax to solve the problem and why the problem itself is
important.

PostgreSQL supports the ability to produce Set Returning Functions and
various other extensions. Would it be possible to change this so that
we don't add new syntax to the parser but rather we do this as a set
of functions?

An alternative might be for us to implement a pluggable parser, so
that you can have an "alternate syntax" plugin. If we did that, you
can then maintain the plugin outside of core until the time when SQL
Standard is updated and we can implement directly. We already support
the ability to invent new plan nodes, so this could be considered as
part of the plugin.

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#34Peter Moser
pitiz29a@gmail.com
In reply to: Thomas Munro (#32)
1 attachment(s)
Re: [PROPOSAL] Temporal query processing with range types

2017-09-01 6:49 GMT+02:00 Thomas Munro <thomas.munro@enterprisedb.com>:

On Tue, Aug 1, 2017 at 12:53 AM, Peter Moser <pitiz29a@gmail.com> wrote:
This patch still applies, but no longer compiles:

nodeTemporalAdjustment.c: In function ‘ExecTemporalAdjustment’:
nodeTemporalAdjustment.c:286:21: error: incompatible types when
assigning to type ‘Form_pg_attribute’ from type
‘FormData_pg_attribute’
node->datumFormat = curr->tts_tupleDescriptor->attrs[tc->attNumP1 - 1];
^

After commits 2cd70845 and c6293249 you need to change expressions of
that format to, for example:

node->datumFormat = TupleDescAttr(curr->tts_tupleDescriptor,
tc->attNumP1 - 1);

Hi Thomas,
thank you for your feedback.

We fixed the mentioned issues and rebased our patch on top of commit
69835bc8988812c960f4ed5aeee86b62ac73602a from "Tue Sep 12 19:27:48
2017 -0400".

Best regards,
Anton, Johann, Michael, Peter

Attachments:

tpg_primitives_out_v8.patchtext/x-patch; charset=US-ASCII; name=tpg_primitives_out_v8.patchDownload
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 4cee357..262524c 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -932,6 +932,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 c1aa506..9d78254 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..d5797d3
--- /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(TupleDescAttr(slot->tts_tupleDescriptor, 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(TupleDescAttr(slot->tts_tupleDescriptor, 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 = TupleDescAttr(curr->tts_tupleDescriptor, 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 = (ExecProcNodeMtd) 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 f1bed14..aa1d43d 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2085,6 +2085,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;
 }
@@ -2465,6 +2467,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)
 {
@@ -2960,6 +2997,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);
 
@@ -3042,6 +3080,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);
@@ -5513,6 +5552,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 8b56b91..9f41784 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 b83d919..89dd148 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1666,6 +1666,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
@@ -1948,6 +1950,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");
@@ -2710,6 +2724,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);
@@ -2786,6 +2801,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");
@@ -2920,6 +2963,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);
@@ -3959,6 +4003,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;
@@ -4070,6 +4117,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 fbf8330..5b1bdb5 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();
 }
@@ -2560,6 +2592,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 2d7e1d8..db9dee1 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 2821662..86498f7 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);
@@ -2398,6 +2408,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;
+}
+
 
 /*****************************************************************************
  *
@@ -5120,6 +5157,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 6b79b3a..e00d632 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1982,6 +1982,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 1103984..3c998fd 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -2768,6 +2768,7 @@ finalize_plan(PlannerInfo *root, Plan *plan,
 		case T_Unique:
 		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 26567cb..c376301 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2542,6 +2542,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 757a4a8..5149a98 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 c303818..129a32e 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> PartitionBoundSpec
 %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
 
@@ -11265,6 +11270,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.
@@ -11534,6 +11552,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;
+				}
 		;
 
 
@@ -15003,7 +15103,8 @@ type_func_name_keyword:
  * forced to.
  */
 reserved_keyword:
-			  ALL
+			  ALIGN
+			| ALL
 			| ANALYSE
 			| ANALYZE
 			| AND
@@ -15051,6 +15152,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..2f01d52
--- /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,
+								   "&&",
+								   (Node *) copyObject(crLargTs),
+								   (Node *) 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 (Node *) 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,
+									"<@",
+									(Node *) copyObject(crP1),
+									(Node *) 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,
+									 "=",
+									 (Node *) copyObject(crUsingItemL),
+									 (Node *) 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 (Node *) 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 d820b56..6bd8a20 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -5154,6 +5154,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 90a60ab..9382895 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1102,6 +1102,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 f3e4c69..0e7f64b 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
@@ -1538,6 +1540,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 a382331..0ab77e2 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 a39e59d..0632517 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 e372f88..11e0f02 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -171,6 +171,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 2fd3f2b..f91b07e 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;
+
+
#35Peter Moser
pitiz29a@gmail.com
In reply to: Simon Riggs (#33)
Re: [PROPOSAL] Temporal query processing with range types

2017-09-12 16:33 GMT+02:00 Simon Riggs <simon@2ndquadrant.com>:

PostgreSQL tries really very hard to implement the SQL Standard and
just the standard. ISTM that the feedback you should have been given
is that this is very interesting but will not be committed in its
current form; I am surprised to see nobody has said that, though you
can see the truth of that since nobody is actively looking to review
or commit this. Obviously if the standard were changed to support
these things we'd suddenly be interested...

Ok, we understand that PostgreSQL wants to strictly follow the SQL
standard, which is not yet defined for temporal databases. In this
context we understand your comment and agree on your position.

Our approach with the two temporal primitives is more far-reaching and
comprehensive: it supports all operators of a temporal relational
algebra by systematically transforming the temporal operators to the
nontemporal counterparts, thereby taking advantage of all features of
the underlying DBMS. This requires of course also new syntax.

What I think I'm lacking is a clear statement of why we need to have
new syntax to solve the problem ...

The newly introduced syntax of the two primitives comes from our
decision to divide a bigger patch into two parts: an primitives-only
patch, and a temporal query rewrite patch. We thought of discussing
and providing a second patch in the future, which would then
automatically rewrite temporal queries into their non-temporal
counterparts using these two primitives.

... and why the problem itself is
important.

The main idea about these temporal primitives is to have only two new
operators to provide the whole range of temporal queries, that is,
temporal joins, temporal set operations, temporal duplicate
elimination, and temporal aggregation. The benefit of the primitives
is that it is minimal invasive to the postgres kernel due to the reuse of all
standard operators after the temporal split (or normalization). It can
therefore also use existing optimizations already implemented.

An alternative approach would be to implement for each operator a
separate algorithm. For instance, Jeff Davis is implementing a temporal
join into the existing Merge Join Executor (see [1]/messages/by-id/CAMp0ubfwAFFW3O_NgKqpRPmm56M4weTEXjprb2gP_NrDaEC4Eg@mail.gmail.com). Note that a
temporal join is the only operator that can be implemented without
introducing new syntax due to the overlap predicate. For all other
temporal operators a discussion about new syntax is necessary anyway,
independent of the implementation approach.

PostgreSQL supports the ability to produce Set Returning Functions and
various other extensions. Would it be possible to change this so that
we don't add new syntax to the parser but rather we do this as a set
of functions?

Set Returning Functions would indeed be a possibility to implement
temporal query processing without new syntax, though it has some serious
drawbacks: the user has to specify the schema of the query results; the
performance might be a problem, since functions are treated as
black-boxes for the optimizer, loosing selection-pushdown and similar
optimizations.

An alternative might be for us to implement a pluggable parser, so
that you can have an "alternate syntax" plugin. If we did that, you
can then maintain the plugin outside of core until the time when SQL
Standard is updated and we can implement directly. We already support
the ability to invent new plan nodes, so this could be considered as
part of the plugin.

This is an interesting idea to look into when it is ready some day.

With all these clarifications in mind, we thought to focus meanwhile
on improving the performance of temporal query processing in special
cases (eg., joins), similar to the pending Range Merge Join patch by
Jeff Davis in [1]/messages/by-id/CAMp0ubfwAFFW3O_NgKqpRPmm56M4weTEXjprb2gP_NrDaEC4Eg@mail.gmail.com. Hereby, we like to contribute to it as reviewers
and hopefully add some improvements or valuable ideas from our
research area.

Best regards,
Anton, Johann, Michael, Peter

[1]: /messages/by-id/CAMp0ubfwAFFW3O_NgKqpRPmm56M4weTEXjprb2gP_NrDaEC4Eg@mail.gmail.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#36Pavel Stehule
pavel.stehule@gmail.com
In reply to: Peter Moser (#35)
Re: [PROPOSAL] Temporal query processing with range types

2017-09-22 9:59 GMT+02:00 Peter Moser <pitiz29a@gmail.com>:

2017-09-12 16:33 GMT+02:00 Simon Riggs <simon@2ndquadrant.com>:

PostgreSQL tries really very hard to implement the SQL Standard and
just the standard. ISTM that the feedback you should have been given
is that this is very interesting but will not be committed in its
current form; I am surprised to see nobody has said that, though you
can see the truth of that since nobody is actively looking to review
or commit this. Obviously if the standard were changed to support
these things we'd suddenly be interested...

Ok, we understand that PostgreSQL wants to strictly follow the SQL
standard, which is not yet defined for temporal databases. In this
context we understand your comment and agree on your position.

ANSI SQL 2011 has temporal data support

https://www.slideshare.net/CraigBaumunk/temporal-extensions-tosql20112012010438

Regards

Pavel Stehule

#37Peter Moser
pitiz29a@gmail.com
In reply to: Pavel Stehule (#36)
Re: [PROPOSAL] Temporal query processing with range types

2017-09-22 10:06 GMT+02:00 Pavel Stehule <pavel.stehule@gmail.com>:

ANSI SQL 2011 has temporal data support

https://www.slideshare.net/CraigBaumunk/temporal-extensions-tosql20112012010438

As operations it only supports temporal inner joins using the overlap predicate.
Temporal aggregation, temporal outer joins, temporal duplicate
elimination, and temporal set operations are not supported in
SQL:2011.
Please see [1]https://cs.ulb.ac.be/public/_media/teaching/infoh415/tempfeaturessql2011.pdf Section 2.5 Future directions.

Best regards,
Anton, Johann, Michael, Peter

[1]: https://cs.ulb.ac.be/public/_media/teaching/infoh415/tempfeaturessql2011.pdf

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#38Pavel Stehule
pavel.stehule@gmail.com
In reply to: Peter Moser (#37)
Re: [PROPOSAL] Temporal query processing with range types

2017-09-22 10:15 GMT+02:00 Peter Moser <pitiz29a@gmail.com>:

2017-09-22 10:06 GMT+02:00 Pavel Stehule <pavel.stehule@gmail.com>:

ANSI SQL 2011 has temporal data support

https://www.slideshare.net/CraigBaumunk/temporal-

extensions-tosql20112012010438

As operations it only supports temporal inner joins using the overlap
predicate.
Temporal aggregation, temporal outer joins, temporal duplicate
elimination, and temporal set operations are not supported in
SQL:2011.
Please see [1] Section 2.5 Future directions.

Best regards,
Anton, Johann, Michael, Peter

[1] https://cs.ulb.ac.be/public/_media/teaching/infoh415/
tempfeaturessql2011.pdf

Thank you for info.

Currently Postgres has zero support for SQL:2011 temporal tables. Isn't
better start with already standard features than appends some without
standard? The standard has some concept and if we start out of this
concept, then the result will be far to standard probably.

Regards

Pavel

#39Peter Moser
pitiz29a@gmail.com
In reply to: Pavel Stehule (#38)
Re: [PROPOSAL] Temporal query processing with range types

2017-09-22 10:21 GMT+02:00 Pavel Stehule <pavel.stehule@gmail.com>:

Currently Postgres has zero support for SQL:2011 temporal tables. Isn't
better start with already standard features than appends some without
standard? The standard has some concept and if we start out of this concept,
then the result will be far to standard probably.

We will focus for now on the Range Merge Join algorithm by Jeff Davis,
which implements a temporal join with overlap predicates.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#40Paul A Jungwirth
pj@illuminatedcomputing.com
In reply to: Anton Dignös (#1)
Re: [PROPOSAL] Temporal query processing with range types

On Fri, Jul 22, 2016 at 4:15 AM, Anton Dignös <dignoes@inf.unibz.it> wrote:

We would like to contribute to PostgreSQL a solution that supports the query
processing of "at each time point". The basic idea is to offer two new
operators, NORMALIZE and ALIGN, whose purpose is to adjust (or split) the
ranges of tuples so that subsequent queries can use the usual grouping and
equality conditions to get the intended results.

I just wanted to chime in and say that the work these people have done
is *amazing*. I read two of their papers yesterday [1, 2], and if you
are interested in temporal data, I encourage you to read them too. The
first one is only 12 pages and quite readable. After that the second
is easy because it covers a lot of the same ground but adds "scaling"
of values when a tuple is split, and some other interesting points.
Their contributions could be used to implement SQL:2011 syntax but go
way beyond that.

Almost every project I work on could use temporal database support,
but there is nothing available in the Open Source world. The
temporal_tables extension [3]https://pgxn.org/dist/temporal_tables/ offers transaction-time support, which
is great for auditing, but it has no valid-time support (aka
application-time or state-time). Same with Magnus Hagander's TARDIS
approach [4]https://www.youtube.com/watch?v=TRgni5q0YM8, Chronomodel [5]https://github.com/ifad/chronomodel (an extension to the Rails ORM), or any
other project I've seen. But valid-time is the more valuable
dimension, because it tells you the history of the thing itself (not
just when the database was changed). Also nothing is even attempting
full bitemporal support.

The ideas behind temporal data are covered extensively in Snodgrass's
1999 book [6]*Developing Time-Oriented Database Applications in SQL*, downloadable for free from https://www2.cs.arizona.edu/~rts/publications.html, which shows how valuable it is to handle temporal data
in a principled way, rather than ad hoc. But that book also
demonstrates how complex the queries become to do things like temporal
foreign key constraints and temporal joins. I was sad to learn that
his proposed TSQL2 was rejected as a standard back in the 90s,
although the critiques by C. J. Date [7]http://citeseerx.ist.psu.edu/viewdoc/download;jsessionid=8CA34414C364C1D859CD0EAE7A714DFF?doi=10.1.1.116.7598&amp;rep=rep1&amp;type=pdf have some merit. In
particular, since TSQL2 used *statement* modifiers, some of the
behavior was unclear or bad when using subqueries, views, and
set-returning functions. It makes more sense to have temporal
*operators*, so alongside inner join you have temporal inner join, and
likewise with temporal left outer join, temporal
union/intersection/difference, temporal aggregation, etc. (I think the
drawbacks of TSQL2 came from pursuing an unachievable goal, which was
to enable seamlessly converting existing non-temporal tables to
temporal without breaking any queries.)

Another unsatisfactory approach at historical data, from the industry
rather than academia, is in chapter 4 and elsewhere of Ralph Kimball's
*Data Warehouse Toolkit* [8]http://www.dsc.ufcg.edu.br/~sampaio/Livros/alph%20Kimball.%20The%20Data%20Warehouse%20Toolkit..%20The%20Complete%20Guide%20to%20Dimensional%20Modelling%20(Wiley,2002)(ISBN%200471200247)(449s).pdf. His first suggestion (Type 1 Dimensions)
is to ignore the problem and overwrite old data with new. His Type 2
approach (make a new row) is better but loses the continuity between
the old row and the new. Type 3 fixes that but supports only one
change, not several. And anyway his ideas are tailored to star-schema
designs so are not as broadly useful. Workarounds like bridge tables
and "put the data in the fact table" are even more wedded to a
star-schema approach. But I think his efforts do show how valuable
historical data is, and how hard it is to handle without built-in
support.

As far as I can tell SQL:2011 avoids the statement modifier problem
(I'm not 100% sure), but it is quite limited, mostly covering
transaction-time semantics and not giving any way to do valid-time
outer joins or aggregations. It is clearly an early first step.
Unfortunately the syntax feels (to me) crippled by over-specificity,
like it will have a hard time growing to support all the things you'd
want to do.

The research by Dignös et al shows how you can define temporal
variants for every operator in the relational algebra, and then
implement them by using just two transformations (ALIGN and NORMALIZE)
combined with the existing non-temporal operators. It has a strong
theoretical basis and avoids the TSQL2 problems with composability.
And unlike SQL:2011 it has a great elegance and completeness I haven't
seen anywhere else.

I believe with range types the approach was to build up useful
primitives rather than jumping straight to a less-factored full
implementation of temporal features. (This in spite of SQL:2011
choosing to model begin/end times as separate columns, not as ranges.
:-) It seems to me the Dignös work follows the same philosophy. Their
ALIGN and NORMALIZE could be used to implement SQL:2011 features, but
they are also useful for much more. In their papers they actually
suggest that these transformations need not be exposed to end-users,
although it was convenient to have access to them for their own
research. I think it'd be great if Postgres's SQL dialect supported
them though, since SQL:2011 leaves out so much.

Anyway, I wanted to thank them for their excellent work, their
generosity, and also their perseverance. ([1]https://files.ifi.uzh.ch/boehlen/Papers/modf174-dignoes.pdf is from 2012 and was
built against Postgres 9.0!) I hope we take their contribution
seriously, because it would truly move Postgres's temporal support
beyond any database on the market.

Yours,
Paul

[1]: https://files.ifi.uzh.ch/boehlen/Papers/modf174-dignoes.pdf

[2]: http://www.zora.uzh.ch/id/eprint/130374/1/Extending_the_kernel.pdf

[3]: https://pgxn.org/dist/temporal_tables/

[4]: https://www.youtube.com/watch?v=TRgni5q0YM8

[5]: https://github.com/ifad/chronomodel

[6]: *Developing Time-Oriented Database Applications in SQL*, downloadable for free from https://www2.cs.arizona.edu/~rts/publications.html
downloadable for free from
https://www2.cs.arizona.edu/~rts/publications.html

[7]: http://citeseerx.ist.psu.edu/viewdoc/download;jsessionid=8CA34414C364C1D859CD0EAE7A714DFF?doi=10.1.1.116.7598&amp;rep=rep1&amp;type=pdf

[8]: http://www.dsc.ufcg.edu.br/~sampaio/Livros/alph%20Kimball.%20The%20Data%20Warehouse%20Toolkit..%20The%20Complete%20Guide%20to%20Dimensional%20Modelling%20(Wiley,2002)(ISBN%200471200247)(449s).pdf

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#41Mike Rylander
mrylander@gmail.com
In reply to: Paul A Jungwirth (#40)
Re: [PROPOSAL] Temporal query processing with range types

On Fri, Oct 6, 2017 at 1:22 PM, Paul A Jungwirth
<pj@illuminatedcomputing.com> wrote:

On Fri, Jul 22, 2016 at 4:15 AM, Anton Dignös <dignoes@inf.unibz.it> wrote:

We would like to contribute to PostgreSQL a solution that supports the query
processing of "at each time point". The basic idea is to offer two new
operators, NORMALIZE and ALIGN, whose purpose is to adjust (or split) the
ranges of tuples so that subsequent queries can use the usual grouping and
equality conditions to get the intended results.

I just wanted to chime in and say that the work these people have done
is *amazing*. I read two of their papers yesterday [1, 2], and if you
are interested in temporal data, I encourage you to read them too. The
first one is only 12 pages and quite readable. After that the second
is easy because it covers a lot of the same ground but adds "scaling"
of values when a tuple is split, and some other interesting points.
Their contributions could be used to implement SQL:2011 syntax but go
way beyond that.

I've also been following this feature with great interest, and would
definitely throw whatever tiny weight I have, sitting out here in the
the peanut gallery, behind accepting the ALIGN and NORMALIZE syntax.
I estimate that about a third of the non-trivial queries in the
primary project I work on (and have, on Postgres, for the last 13+
years) would be simpler with support of the proposed syntax, and some
of the most complex business logic would be simplified nearly to the
point of triviality.

Anyway, that's my $0.02.

Thank you, Anton and Peter!

-- Mike

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#42Robert Haas
robertmhaas@gmail.com
In reply to: Mike Rylander (#41)
Re: [PROPOSAL] Temporal query processing with range types

On Fri, Oct 6, 2017 at 3:22 PM, Mike Rylander <mrylander@gmail.com> wrote:

I've also been following this feature with great interest, and would
definitely throw whatever tiny weight I have, sitting out here in the
the peanut gallery, behind accepting the ALIGN and NORMALIZE syntax.
I estimate that about a third of the non-trivial queries in the
primary project I work on (and have, on Postgres, for the last 13+
years) would be simpler with support of the proposed syntax, and some
of the most complex business logic would be simplified nearly to the
point of triviality.

This is really good input. If the feature weren't useful, then it
wouldn't make sense to try to figure out how to integrate it, but if
it is, then we should try.

I don't think that implementing a feature like this by SQL
transformation can work. It's certainly got the advantage of
simplicity of implemention, but there are quite a few things that seem
like they won't always work correctly. For instance:

+ * 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

One problem with this is that we end up looking up functions in
pg_catalog by name: LOWER, UPPER, LEAST, GREATEST. In particular,
when we do this...

+    fcUpperRarg = makeFuncCall(SystemFuncName("upper"),
+                               list_make1(crRargTs),
+                               UNKNOWN_LOCATION);

...we're hoping and praying that we're going to latch onto the first of these:

rhaas=# \df upper
List of functions
Schema | Name | Result data type | Argument data types | Type
------------+-------+------------------+---------------------+--------
pg_catalog | upper | anyelement | anyrange | normal
pg_catalog | upper | text | text | normal
(2 rows)

But that's only true as long as there isn't another function in
pg_catalog with a match to the specific range type that is being used
here, and there's nothing to stop a user from creating one, and then
their query, which does not anywhere in its query text mention the
name of that function, will start failing. We're not going to accept
that limitation. Looking up functions by name rather than by OID or
using an opclass or something is pretty much a death sentence for a
core feature, and this patch does a lot of it.

A related problem is that, because all of this transformation is being
done in the parser, when you use this temporal syntax to create a
view, and then run pg_dump to dump that view, you are going to (I
think) get the transformed version, not the original. Things like
transformTemporalClause are going to have user-visible effects: the
renaming you do there will (I think) be reflected in the deparsed
output of views. That's not good. Users have a right to expect that
what comes out of deparsing will at least resemble what they put in.

Error reporting might be a problem too: makeTemporalQuerySkeleton is
creating parse nodes that have no location, so if an error develops at
that point, how will the user correlate that with what they typed in?

I suspect there are also problems with plan invalidation. Any
decisions we make at parse time are fixed forever. DDL changes can
force a re-plan, but not a re-parse.

Overall, I think that the whole approach here probably needs to be
scrapped and rethought. The stuff this patch is doing really belongs
in the optimizer, not the parser, I think. It could possibly happen
at a relatively early stage in the optimizer so that the rest of the
optimizer can see the results of the transformation and, well,
optimize. But parse time is way too early.

Unrelated to the above, this patch introduces various kinds of helper
functions which are general-purpose in function but dumped in with the
temporal support because it happens to use them. For instance:

+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;
+}

A function as general as typeGet() certainly does not belong in
parse_clause.c in the middle of a long list of temporal functions.
This particular function is also a bad idea in general, because typtup
is only a valid pointer until ReleaseSysCache() is called. This will
appear to work normally because, normally, no relevant cache
invalidation messages will show up at the wrong time, but if one does
then typtup will be pointing off into outer space. You can find bugs
like this by testing with CLOBBER_CACHE_ALWAYS defined (warning: this
makes things very slow). But the general point I want to make here is
actually about inventing your own idioms vs. using the ones we've got
-- if you're going to invent new general-purpose primitives, they need
to go next to the existing functions that do similar things, not in
whatever part of the code you first decided you needed them.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#43Peter Moser
peter.moser@unibz.it
In reply to: Paul A Jungwirth (#40)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

2017-10-06 19:22 GMT+02:00 Paul A Jungwirth <pj@illuminatedcomputing.com>:

I just wanted to chime in and say that the work these people have done
is *amazing*. I read two of their papers yesterday [1, 2], and if you
are interested in temporal data, I encourage you to read them too. The
first one is only 12 pages and quite readable. After that the second
is easy because it covers a lot of the same ground but adds "scaling"
of values when a tuple is split, and some other interesting points.
Their contributions could be used to implement SQL:2011 syntax but go
way beyond that.

Almost every project I work on could use temporal database support,
but there is nothing available in the Open Source world. The
temporal_tables extension [3] offers transaction-time support, which
is great for auditing, but it has no valid-time support (aka
application-time or state-time). Same with Magnus Hagander's TARDIS
approach [4], Chronomodel [5] (an extension to the Rails ORM), or any
other project I've seen. But valid-time is the more valuable
dimension, because it tells you the history of the thing itself (not
just when the database was changed). Also nothing is even attempting
full bitemporal support.

The ideas behind temporal data are covered extensively in Snodgrass's
1999 book [6], which shows how valuable it is to handle temporal data
in a principled way, rather than ad hoc. But that book also
demonstrates how complex the queries become to do things like temporal
foreign key constraints and temporal joins. I was sad to learn that
his proposed TSQL2 was rejected as a standard back in the 90s,
although the critiques by C. J. Date [7] have some merit. In
particular, since TSQL2 used *statement* modifiers, some of the
behavior was unclear or bad when using subqueries, views, and
set-returning functions. It makes more sense to have temporal
*operators*, so alongside inner join you have temporal inner join, and
likewise with temporal left outer join, temporal
union/intersection/difference, temporal aggregation, etc. (I think the
drawbacks of TSQL2 came from pursuing an unachievable goal, which was
to enable seamlessly converting existing non-temporal tables to
temporal without breaking any queries.)

Another unsatisfactory approach at historical data, from the industry
rather than academia, is in chapter 4 and elsewhere of Ralph Kimball's
*Data Warehouse Toolkit* [8]. His first suggestion (Type 1 Dimensions)
is to ignore the problem and overwrite old data with new. His Type 2
approach (make a new row) is better but loses the continuity between
the old row and the new. Type 3 fixes that but supports only one
change, not several. And anyway his ideas are tailored to star-schema
designs so are not as broadly useful. Workarounds like bridge tables
and "put the data in the fact table" are even more wedded to a
star-schema approach. But I think his efforts do show how valuable
historical data is, and how hard it is to handle without built-in
support.

As far as I can tell SQL:2011 avoids the statement modifier problem
(I'm not 100% sure), but it is quite limited, mostly covering
transaction-time semantics and not giving any way to do valid-time
outer joins or aggregations. It is clearly an early first step.
Unfortunately the syntax feels (to me) crippled by over-specificity,
like it will have a hard time growing to support all the things you'd
want to do.

The research by Dignös et al shows how you can define temporal
variants for every operator in the relational algebra, and then
implement them by using just two transformations (ALIGN and NORMALIZE)
combined with the existing non-temporal operators. It has a strong
theoretical basis and avoids the TSQL2 problems with composability.
And unlike SQL:2011 it has a great elegance and completeness I haven't
seen anywhere else.

I believe with range types the approach was to build up useful
primitives rather than jumping straight to a less-factored full
implementation of temporal features. (This in spite of SQL:2011
choosing to model begin/end times as separate columns, not as ranges.
:-) It seems to me the Dignös work follows the same philosophy. Their
ALIGN and NORMALIZE could be used to implement SQL:2011 features, but
they are also useful for much more. In their papers they actually
suggest that these transformations need not be exposed to end-users,
although it was convenient to have access to them for their own
research. I think it'd be great if Postgres's SQL dialect supported
them though, since SQL:2011 leaves out so much.

Anyway, I wanted to thank them for their excellent work, their
generosity, and also their perseverance. ([1] is from 2012 and was
built against Postgres 9.0!) I hope we take their contribution
seriously, because it would truly move Postgres's temporal support
beyond any database on the market.

Paul, you are spot on. Your comments are really insightful and the
understanding of the pros and cons of the different solutions is
impressive.

Some additional background about our approach:

During more than 20 years of research in the temporal database area we
have seen many ideas that at the end did not make a real difference for
temporal query processing. Our goal was to identify and implement the
basic functionality that database systems must offer to support the
processing of temporal data. Our answer to this is the adjustment of
time ranges with ALIGN and NORMALIZE. We believe these are general and
useful primitives that will greatly benefit any possible temporal
extension of SQL.

We have been using ALIGN and NORMALIZE since several years in our
teaching at different universities. Students have been using the
patched PostgreSQL kernel and everything (understanding the concepts;
using the primitives in numerous examples) has worked out really well.
As you have noticed the primitives were also well-received in the
flagship outlets of the database research community.

Best regards,
Anton, Johann, Michael, Peter

Show quoted text

[1] https://files.ifi.uzh.ch/boehlen/Papers/modf174-dignoes.pdf

[2] http://www.zora.uzh.ch/id/eprint/130374/1/Extending_the_kernel.pdf

[3] https://pgxn.org/dist/temporal_tables/

[4] https://www.youtube.com/watch?v=TRgni5q0YM8

[5] https://github.com/ifad/chronomodel

[6] *Developing Time-Oriented Database Applications in SQL*,
downloadable for free from
https://www2.cs.arizona.edu/~rts/publications.html

[7] http://citeseerx.ist.psu.edu/viewdoc/download;jsessionid=8CA34414C364C1D859CD0EAE7A714DFF?doi=10.1.1.116.7598&amp;rep=rep1&amp;type=pdf

[8] http://www.dsc.ufcg.edu.br/~sampaio/Livros/alph%20Kimball.%20The%20Data%20Warehouse%20Toolkit..%20The%20Complete%20Guide%20to%20Dimensional%20Modelling%20(Wiley,2002)(ISBN%200471200247)(449s).pdf

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#44Peter Moser
peter.moser@unibz.it
In reply to: Robert Haas (#42)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

2017-11-11 13:19 GMT+01:00 Robert Haas <robertmhaas@gmail.com>:

This is really good input. If the feature weren't useful, then it
wouldn't make sense to try to figure out how to integrate it, but if
it is, then we should try.

We are happy to hear this and will do the implementation. Any input
regarding the implementation is much appreciated.

I don't think that implementing a feature like this by SQL
transformation can work. It's certainly got the advantage of
simplicity of implemention, but there are quite a few things that seem
like they won't always work correctly.
[...]
Overall, I think that the whole approach here probably needs to be
scrapped and rethought. The stuff this patch is doing really belongs
in the optimizer, not the parser, I think. It could possibly happen
at a relatively early stage in the optimizer so that the rest of the
optimizer can see the results of the transformation and, well,
optimize. But parse time is way too early.

We create this query rewrites during parser stage, because we want
that the optimizer chooses the best strategies for each rewritten
subplan and that our executor nodes get the desired input format in
the most optimal way. Our goal was an integration that re-uses the
existing PostgreSQL rewrites and optimizations fully.

Another approach is to optimize the temporal primitives manually.
This does not reuse existing PostgreSQL optimizations automatically.

Is there a general guideline or policy as to which approach is
preferable?

Regarding all the other issues, we will look into them in detail and
report back soon.

Best regards,
Anton, Johann, Michael, Peter

#45Tom Lane
tgl@sss.pgh.pa.us
In reply to: Peter Moser (#44)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

Peter Moser <peter.moser@unibz.it> writes:

2017-11-11 13:19 GMT+01:00 Robert Haas <robertmhaas@gmail.com>:

Overall, I think that the whole approach here probably needs to be
scrapped and rethought. The stuff this patch is doing really belongs
in the optimizer, not the parser, I think. It could possibly happen
at a relatively early stage in the optimizer so that the rest of the
optimizer can see the results of the transformation and, well,
optimize. But parse time is way too early.

We create this query rewrites during parser stage, because we want
that the optimizer chooses the best strategies for each rewritten
subplan and that our executor nodes get the desired input format in
the most optimal way. Our goal was an integration that re-uses the
existing PostgreSQL rewrites and optimizations fully.

Robert is correct that putting this into the parser is completely the
wrong thing. If you do that, then for example views using the features
will reverse-list in the rewritten form, which we Do Not Want, even
if the rewritten form is completely valid SQL (is it?).

You might consider putting the rewriting into, um, the rewriter.
It could be a separate pass after view expansion, if direct integration
with the existing behavior seems unduly spaghetti-ish. Or do it in
an early phase of planning as he suggested. There's not really that
much difference between the rewriter and the planner for this purpose.
Although one way to draw the distinction is that the output of the
rewriter is (currently) still fully expressible as plain SQL, whereas
once the planner goes into action the intermediate states of the tree
might not really be SQL anymore (eg, it might contain join types that
don't correspond to any SQL syntax). So depending on what your rewrite
emits, there would be a weak preference for calling it part of the
rewriter or planner respectively.

regards, tom lane

#46Peter Moser
peter.moser@unibz.it
In reply to: Tom Lane (#45)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

2017-11-14 18:42 GMT+01:00 Tom Lane <tgl@sss.pgh.pa.us>:

Robert is correct that putting this into the parser is completely the
wrong thing. If you do that, then for example views using the features
will reverse-list in the rewritten form, which we Do Not Want, even
if the rewritten form is completely valid SQL (is it?).

Yes, the subnode to our executor is rewritten in valid SQL.

You might consider putting the rewriting into, um, the rewriter.
It could be a separate pass after view expansion, if direct integration
with the existing behavior seems unduly spaghetti-ish. Or do it in
an early phase of planning as he suggested. There's not really that
much difference between the rewriter and the planner for this purpose.
Although one way to draw the distinction is that the output of the
rewriter is (currently) still fully expressible as plain SQL, whereas
once the planner goes into action the intermediate states of the tree
might not really be SQL anymore (eg, it might contain join types that
don't correspond to any SQL syntax). So depending on what your rewrite
emits, there would be a weak preference for calling it part of the
rewriter or planner respectively.

Thank you for your feedback. We'll have a look at this and come back to you.

#47Robert Haas
robertmhaas@gmail.com
In reply to: Peter Moser (#46)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On Thu, Nov 16, 2017 at 3:32 AM, Peter Moser <peter.moser@unibz.it> wrote:

Thank you for your feedback. We'll have a look at this and come back to you.

Another thing to think about is that even though the CURRENT
implementation just rewrites the relevant constructs as SQL, in the
future somebody might want to do something else. I feel like it's not
hard to imagine a purpose-build ALIGN or NORMALIZE join type being a
lot faster than the version that's just done by rewriting the SQL.
That would be more work, potentially, but it would be nice if the
initial implementation leant itself to be extended that way in the
future, which an all-rewriter implementation would not. On the other
hand, maybe an early-in-the-optimizer implementation wouldn't either,
and maybe it's not worth worrying about it anyway. But it would be
cool if this worked out in a way that meant it could be further
improved without having to change it completely.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#48Peter Moser
pitiz29a@gmail.com
In reply to: Robert Haas (#47)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

2017-11-14 18:42 GMT+01:00 Tom Lane <tgl@sss.pgh.pa.us>:

You might consider putting the rewriting into, um, the rewriter.
It could be a separate pass after view expansion, if direct integration
with the existing behavior seems unduly spaghetti-ish.  Or do it in
an early phase of planning as he suggested.  There's not really that
much difference between the rewriter and the planner for this purpose.
Although one way to draw the distinction is that the output of the
rewriter is (currently) still fully expressible as plain SQL, whereas
once the planner goes into action the intermediate states of the tree
might not really be SQL anymore (eg, it might contain join types that
don't correspond to any SQL syntax).  So depending on what your rewrite
emits, there would be a weak preference for calling it part of the
rewriter or planner respectively.

2017-11-16 16:42 GMT+01:00 Robert Haas <robertmhaas@gmail.com>:

Another thing to think about is that even though the CURRENT
implementation just rewrites the relevant constructs as SQL, in the
future somebody might want to do something else.  I feel like it's not
hard to imagine a purpose-build ALIGN or NORMALIZE join type being a
lot faster than the version that's just done by rewriting the SQL.
That would be more work, potentially, but it would be nice if the
initial implementation leant itself to be extended that way in the
future, which an all-rewriter implementation would not.  On the other
hand, maybe an early-in-the-optimizer implementation wouldn't either,
and maybe it's not worth worrying about it anyway.  But it would be
cool if this worked out in a way that meant it could be further
improved without having to change it completely.

Hi hackers,
we like to rethink our approach...

For simplicity I'll drop ALIGN for the moment and focus solely on NORMALIZE:

    SELECT * FROM (R NORMALIZE S ON R.x = S.y WITH (R.time, S.time)) c;

Our normalization executor node needs the following input (for now
expressed in plain SQL):

    SELECT R.*, p1
    FROM (SELECT *, row_id() OVER () rn FROM R) R
LEFT OUTER JOIN (
    SELECT y, LOWER(time) p1 FROM S
    UNION
    SELECT y, UPPER(time) p1 FROM S
) S
ON R.x = S.y AND p1 <@ R.time
    ORDER BY rn, p1;

In other words:
1) The left subquery adds an unique ID to each tuple (i.e., rn).
2) The right subquery creates two results for each input tuple: one for
   the upper and one for the lower bound of each input tuple's valid time
   column. The boundaries get put into a single (scalar) column, namely p1.
3) We join both subqueries if the normalization predicates hold (R.x = S.y)
   and p1 is inside the time of the current outer tuple.
4) Finally, we sort the result by the unique ID (rn) and p1, and give all
   columns of the outer relation, rn and p1 back.

Our first attempt to understand the new approach would be as follows: The
left base rel of the inner left-outer-join can be expressed as a WindowAgg
node. However, the right query of the join is much more difficult to build
(maybe through hash aggregates). Both queries could be put together with a
MergeJoin for instance. However, if we create the plan tree by hand and
choose algorithms for it manually, how is it possible to have it optimized
later? Or, if that is not possible, how do we choose the best algorithms
for it?

Best regards,
Anton, Johann, Michael, Peter

#49Michael Paquier
michael.paquier@gmail.com
In reply to: Peter Moser (#48)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On Tue, Nov 21, 2017 at 6:36 PM, Peter Moser <pitiz29a@gmail.com> wrote:

2017-11-14 18:42 GMT+01:00 Tom Lane <tgl@sss.pgh.pa.us>:

You might consider putting the rewriting into, um, the rewriter.
It could be a separate pass after view expansion, if direct integration
with the existing behavior seems unduly spaghetti-ish. Or do it in
an early phase of planning as he suggested. There's not really that
much difference between the rewriter and the planner for this purpose.
Although one way to draw the distinction is that the output of the
rewriter is (currently) still fully expressible as plain SQL, whereas
once the planner goes into action the intermediate states of the tree
might not really be SQL anymore (eg, it might contain join types that
don't correspond to any SQL syntax). So depending on what your rewrite
emits, there would be a weak preference for calling it part of the
rewriter or planner respectively.

2017-11-16 16:42 GMT+01:00 Robert Haas <robertmhaas@gmail.com>:

Another thing to think about is that even though the CURRENT
implementation just rewrites the relevant constructs as SQL, in the
future somebody might want to do something else. I feel like it's not
hard to imagine a purpose-build ALIGN or NORMALIZE join type being a
lot faster than the version that's just done by rewriting the SQL.
That would be more work, potentially, but it would be nice if the
initial implementation leant itself to be extended that way in the
future, which an all-rewriter implementation would not. On the other
hand, maybe an early-in-the-optimizer implementation wouldn't either,
and maybe it's not worth worrying about it anyway. But it would be
cool if this worked out in a way that meant it could be further
improved without having to change it completely.

Hi hackers,
we like to rethink our approach...

For simplicity I'll drop ALIGN for the moment and focus solely on NORMALIZE:

SELECT * FROM (R NORMALIZE S ON R.x = S.y WITH (R.time, S.time)) c;

Our normalization executor node needs the following input (for now
expressed in plain SQL):

SELECT R.*, p1
FROM (SELECT *, row_id() OVER () rn FROM R) R
LEFT OUTER JOIN (
SELECT y, LOWER(time) p1 FROM S
UNION
SELECT y, UPPER(time) p1 FROM S
) S
ON R.x = S.y AND p1 <@ R.time
ORDER BY rn, p1;

In other words:
1) The left subquery adds an unique ID to each tuple (i.e., rn).
2) The right subquery creates two results for each input tuple: one for
the upper and one for the lower bound of each input tuple's valid time
column. The boundaries get put into a single (scalar) column, namely p1.
3) We join both subqueries if the normalization predicates hold (R.x = S.y)
and p1 is inside the time of the current outer tuple.
4) Finally, we sort the result by the unique ID (rn) and p1, and give all
columns of the outer relation, rn and p1 back.

Our first attempt to understand the new approach would be as follows: The
left base rel of the inner left-outer-join can be expressed as a WindowAgg
node. However, the right query of the join is much more difficult to build
(maybe through hash aggregates). Both queries could be put together with a
MergeJoin for instance. However, if we create the plan tree by hand and
choose algorithms for it manually, how is it possible to have it optimized
later? Or, if that is not possible, how do we choose the best algorithms
for it?

As far as I can see, this patch has received some feedback. In order
to digest them properly, I am marking the patch as returned with
feedback.
--
Michael

#50Robert Haas
robertmhaas@gmail.com
In reply to: Peter Moser (#48)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On Tue, Nov 21, 2017 at 4:36 AM, Peter Moser <pitiz29a@gmail.com> wrote:

Hi hackers,
we like to rethink our approach...

For simplicity I'll drop ALIGN for the moment and focus solely on NORMALIZE:

SELECT * FROM (R NORMALIZE S ON R.x = S.y WITH (R.time, S.time)) c;

Our normalization executor node needs the following input (for now
expressed in plain SQL):

SELECT R.*, p1
FROM (SELECT *, row_id() OVER () rn FROM R) R
LEFT OUTER JOIN (
SELECT y, LOWER(time) p1 FROM S
UNION
SELECT y, UPPER(time) p1 FROM S
) S
ON R.x = S.y AND p1 <@ R.time
ORDER BY rn, p1;

In other words:
1) The left subquery adds an unique ID to each tuple (i.e., rn).
2) The right subquery creates two results for each input tuple: one for
the upper and one for the lower bound of each input tuple's valid time
column. The boundaries get put into a single (scalar) column, namely p1.
3) We join both subqueries if the normalization predicates hold (R.x = S.y)
and p1 is inside the time of the current outer tuple.
4) Finally, we sort the result by the unique ID (rn) and p1, and give all
columns of the outer relation, rn and p1 back.

So, if I understand correctly, there are three possible outcomes. If
S.time and R.time are non-overlapping, then a null-extended row is
produced with the original data from R. If S.time overlaps R.time but
is not contained within it, then this produces one result row, not
null-extended -- either the upper or lower bound will found to be
contained within R.time, but the other will not. If S.time overlaps
R.time but is not contained within it, then this produces 2 result
rows, one with p1 containing S.time's lower bound and one with p1
containing S.time's upper bound. Is that right? Then, after all this
happens, the temporal normalizer code runs over the results and uses
the p1 values as split points for the ranges from R.

I wonder if we could instead think about R NORMALIZE S ON R.x = S.y
WITH (R.time, S.time) as an ordinary join on R.x = S.y with some extra
processing afterwards. After finding all the join partners for a row
in R, extract all the lower and upper bounds from the S.time fields of
the join partners and use that to emit as many rows from R as may be
necessary. The main problem that I see with that approach is that it
seems desirable to separate the extra-processing-afterwards step into
a separate executor node, and there's no way for a separate type of
plan node to know when the join advances the outer side of the join.
That's the purpose that row_id() is serving as you have it; we'd have
to invent some other kind of signalling mechanism, which might not be
entirely trivial. :-(

If we could do it, though, it might be quite a bit more efficient,
because it would avoid scanning S twice and performing a UNION on the
results of those scans. Also, we wouldn't necessarily need to sort
the whole set of rows from R, which I suspect is unavoidable in your
implementation. We'd just need to sort the individual groups of rows
from S, and my guess is that in many practical cases those groups are
fairly small.

I wonder what identities hold for NORMALIZE. It does not seem to be
commutative, but it looks like it might be associative... i.e. (R
NORMALIZE S) NORMALIZE T produces the same output as R NORMALIZE (S
NORMALIZE T), perhaps?

Our first attempt to understand the new approach would be as follows: The
left base rel of the inner left-outer-join can be expressed as a WindowAgg
node. However, the right query of the join is much more difficult to build
(maybe through hash aggregates). Both queries could be put together with a
MergeJoin for instance. However, if we create the plan tree by hand and
choose algorithms for it manually, how is it possible to have it optimized
later? Or, if that is not possible, how do we choose the best algorithms
for it?

You don't want to generate plan nodes directly like this, I think.
Generally, you create paths, and the cheapest path wins at the end of
planning. The thing that makes this tricky is that the temporal
normalization phase can be separated from the inner left-outer-join by
other joins, and the planner doesn't really have a good way of
representing that concept.

More broadly, the very idea of creating plan nodes suggests that you
are approaching this from the point of view of sticking the logic in
near the end of the planning process, which I think is not going to
work. Whatever is going to happen here needs to happen at path
generation time at the latest, or maybe even earlier, before the
optimizer even starts processing the join tree.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#51Simon Riggs
simon@2ndquadrant.com
In reply to: Robert Haas (#50)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On 30 November 2017 at 17:26, Robert Haas <robertmhaas@gmail.com> wrote:

I wonder if we could instead think about R NORMALIZE S ON R.x = S.y
WITH (R.time, S.time) as an ordinary join on R.x = S.y with some extra
processing afterwards.

That would work nicely, kindof like a Projection, but one that can
vary the number of rows emitted.

For normal joins, we simply emit one row. For new style joins we call
a special PostJoinSetProjection function: one tuple in, potentially
many tuples out.

Peter, does Robert's proposed treatment give you what you need?

Overall, I like the goals expressed on this thread. I think if we
should focus on introducing useful new functionality, rather than
focusing on syntax.

I'm not very keen on adopting new syntax that isn't in the
SQLStandard. They have a bad habit of doing something completely
different. So a flexible approach will allow us to have functionality
now and we can adopt any new syntax later.

For any new syntax, I think the right approach would be to create a
new parser plugin. That way we could make all of this part of an
extension.
* a parser plugin for any new syntax
* various PostJoinSetProjection() functions to be called as needed
So the idea is we enable Postgres to allow major new functionality, as
was done for PostGIS so successfully.

We can adopt syntax into the main parser later once SQLStandard
accepts this, or some munged version of it.

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#52Simon Riggs
simon@2ndquadrant.com
In reply to: Peter Moser (#29)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On 30 March 2017 at 13:11, Peter Moser <pitiz29a@gmail.com> wrote:

2017-03-01 10:56 GMT+01:00 Peter Moser <pitiz29a@gmail.com>:

A similar walkthrough for ALIGN will follow soon.

We are thankful for any suggestion or ideas, to be used to write a
good SGML documentation.

The attached README explains the ALIGN operation step-by-step with a
TEMPORAL LEFT OUTER JOIN example. That is, we start from a query
input, show how we rewrite it during parser stage, and show how the
final execution generates result tuples.

Sorry, this was too complex for me.

Can we get a much simpler example please?

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#53Robert Haas
robertmhaas@gmail.com
In reply to: Simon Riggs (#51)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On Sat, Jan 6, 2018 at 3:29 PM, Simon Riggs <simon@2ndquadrant.com> wrote:

I'm not very keen on adopting new syntax that isn't in the
SQLStandard. They have a bad habit of doing something completely
different. So a flexible approach will allow us to have functionality
now and we can adopt any new syntax later.

For any new syntax, I think the right approach would be to create a
new parser plugin. That way we could make all of this part of an
extension.
* a parser plugin for any new syntax
* various PostJoinSetProjection() functions to be called as needed
So the idea is we enable Postgres to allow major new functionality, as
was done for PostGIS so successfully.

We can adopt syntax into the main parser later once SQLStandard
accepts this, or some munged version of it.

We don't currently have any concept of a parser plugin, and I don't
think it's reasonable for this patch to try to invent one. I can't
see where you could possibly put a hook that would handle something
like this. I've thought about suggesting a hook that gets called if
parsing fails, before we give up and throw a syntax error. That
would, perhaps, allow extension to implement additional DDL commands,
which would be pretty cool. However, it wouldn't be practical to use
something like that for this because this is syntax that appears
buried deep down inside FROM clauses, and you'd basically have to
reimplement the core parser's entire handling of SELECT statements,
which would be immensely complicated to develop and a real pain to
maintain.

Furthermore, the changes here aren't only parsing changes. There is a
proposal to add a new executor node which has to be supported by new
planner code. A great deal more than parser pluggability would be
needed. And if we did all that, it doesn't really solve anything
anyway: the potential problem with committing to this syntax is that
if we change it later, there could be upgrade problems.
Extension-izing this wouldn't make those problems go away. People are
going to want to be able to run pg_dump on their old database and
restore the result on their new database, full stop. If we add this
syntax, in core or in a hypothetical extension system, we're stuck
with it and must maintain it going forward.

I also don't agree with the idea that we should reject syntax that
doesn't appear in the SQL standard. We have a great deal of such
syntax already, and we add more of it in every release, and a good
deal of it is contributed by you and your colleagues. I don't see why
this patch should be held to a stricter standard than we do in
general. I agree that there is some possibility for pain if the SQL
standards committee adopts syntax that is similar to whatever we pick
but different in detail, but I don't think we should be too worried
about that unless other database systems, such as Oracle, have syntax
that is similar to what is proposed here but different in detail. The
SQL standards committee seems to like standardizing on whatever
companies with a lot of money have already implemented; it's unlikely
that they are going to adopt something totally different from any
existing system but inconveniently similar to ours.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#54Peter Moser
pitiz29a@gmail.com
In reply to: Robert Haas (#50)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On Thu, 2017-11-30 at 12:26 -0500, Robert Haas wrote:

I wonder if we could instead think about R NORMALIZE S ON R.x = S.y
WITH (R.time, S.time) as an ordinary join on R.x = S.y with some
extra processing afterwards. After finding all the join partners for
a row in R, extract all the lower and upper bounds from the S.time
fields of the join partners and use that to emit as many rows from R
as may be necessary.

We have now a new approach to plan and execute NORMALIZE as a special
join node of type NORMALIZE, an append-plan on the inner join path,
and a merge-join executor. For the latter, we would need to
extend nodeMergejoin.c with an point-in-range-containment join.

That is, we create a new join path within sort_inner_and_outer
(joinpath.c). First two projection nodes to extract the start- and
end-timepoints of the range type used as interval, and above an
append-plan to merge both subplans. In detail, outer_path is just sort,
whereas inner_path is append of (B, ts) projection with (B, te)
projection.
Hereby, B is a set of non-temporal attributes used in join equality
predicates, and [ts,te) forms the valid-time interval. Non-equality
predicates must be handled separately as a filter step.

Do you think, it is a good idea to extend the sort_inner_and_outer()
with a new branch, where jointype == NORMALIZE and add the projection
and append sub-paths there?

The main problem that I see with that approach is that it
seems desirable to separate the extra-processing-afterwards step into
a separate executor node, and there's no way for a separate type of
plan node to know when the join advances the outer side of the join.
That's the purpose that row_id() is serving as you have it; we'd have
to invent some other kind of signalling mechanism, which might not be
entirely trivial. :-(

One advantage of the new approach is that row_id() is no longer needed.
The executor knows that it is inside a new "group" after every NEXTOUTER,
then the whole loop of the inner relation has the same row_id. The whole
mechanism could be handled through the MergeJoinState struct.

If we could do it, though, it might be quite a bit more efficient,
because it would avoid scanning S twice and performing a UNION on the
results of those scans. Also, we wouldn't necessarily need to sort
the whole set of rows from R, which I suspect is unavoidable in your
implementation. We'd just need to sort the individual groups of rows
from S, and my guess is that in many practical cases those groups are
fairly small.

We would need a special SEQSCAN node/executor, that does the projection
and append steps in a single read. Do you have any suggestions how to
implement this?

I wonder what identities hold for NORMALIZE. It does not seem to be
commutative, but it looks like it might be associative... i.e. (R
NORMALIZE S) NORMALIZE T produces the same output as R NORMALIZE (S
NORMALIZE T), perhaps?

It is not commutative and also not associative.
Assume relations that contain one tuple each as follows:

R={[1, 100)}, S={[50, 100)}, and T={[10, 20)}

(R NORMALIZE S) NORMALIZE T gives {[1, 10), [10, 20), [20,50), [50, 100)}

while

R NORMALIZE (S NORMALIZE T) gives {[1, 50), [50, 100)}

Best regards,
Anton, Johann, Michael, Peter

#55Peter Moser
pitiz29a@gmail.com
In reply to: Simon Riggs (#51)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On Sat, 2018-01-06 at 20:29 +0000, Simon Riggs wrote:

* various PostJoinSetProjection() functions to be called as needed
So the idea is we enable Postgres to allow major new functionality,
as was done for PostGIS so successfully.

If we use a post-join approach many intermediate result tuples, that do
not contribute to the final result would be emmitted, and thus the
performance would suffer.

Best regards,
Anton, Johann, Michael, Peter

#56Peter Moser
pitiz29a@gmail.com
In reply to: Simon Riggs (#52)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On Sun, 2018-01-07 at 09:58 +0000, Simon Riggs wrote:

The attached README explains the ALIGN operation step-by-step with
a TEMPORAL LEFT OUTER JOIN example. That is, we start from a query
input, show how we rewrite it during parser stage, and show how the
final execution generates result tuples.

Sorry, this was too complex for me.

Can we get a much simpler example please?

Please see the following simpler example:

DROP TABLE budg;
CREATE TABLE budg(name VARCHAR(5), amnt INTEGER, t DATERANGE);
INSERT INTO budg VALUES ('Joe', 5, '[2012/2/1,2012/9/1)');
INSERT INTO budg VALUES ('Ann', 7, '[2012/5/1,2012/9/1)');
INSERT INTO budg VALUES ('Per', 3, '[2012/4/1,2012/10/1)');
SELECT * FROM budg AS r;

SELECT *
FROM ( budg r ALIGN budg s ON s.amnt > r.amnt WITH (t,t)) r
WHERE NOT EXISTS (
SELECT *
FROM (budg s ALIGN budg r ON s.amnt > r.amnt WITH (t,t)) s
WHERE s.amnt > r.amnt
AND r.t = s.t );

-- name | amnt | t
-- ------+------+-------------------------
-- Joe | 5 | [2012-02-01,2012-05-01)
-- Ann | 7 | [2012-05-01,2012-09-01)
-- Per | 3 | [2012-09-01,2012-10-01)

Best regards,
Anton, Johann, Michael, Peter

#57Peter Moser
pitiz29a@gmail.com
In reply to: Robert Haas (#53)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On Mon, 2018-01-08 at 11:18 -0500, Robert Haas wrote:

I also don't agree with the idea that we should reject syntax that
doesn't appear in the SQL standard. We have a great deal of such
syntax already, and we add more of it in every release, and a good
deal of it is contributed by you and your colleagues. I don't see
why this patch should be held to a stricter standard than we do in
general. I agree that there is some possibility for pain if the SQL
standards committee adopts syntax that is similar to whatever we pick
but different in detail, but I don't think we should be too worried
about that unless other database systems, such as Oracle, have syntax
that is similar to what is proposed here but different in
detail. The
SQL standards committee seems to like standardizing on whatever
companies with a lot of money have already implemented; it's unlikely
that they are going to adopt something totally different from any
existing system but inconveniently similar to ours.

We agree with you.

Best regards,
Anton, Johann, Michael, Peter

#58Peter Moser
pitiz29a@gmail.com
In reply to: Peter Moser (#54)
1 attachment(s)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On 01/26/2018 07:55 AM, Peter Moser wrote:

We have now a new approach to plan and execute NORMALIZE as a special
join node of type NORMALIZE, an append-plan on the inner join path,
and a merge-join executor. For the latter, we would need to
extend nodeMergejoin.c with an point-in-range-containment join.

We are ready with a new prototype for the temporal NORMALIZE operation.
In this prototype we do not rewrite queries as in the previous patch,
but have one executor node, that solves the normalize operation. This
executor is based on a merge-join.

Our new patch is based on top of
75f7855369ec56d4a8e7d6eae98aff1182b85cac from September 6, 2018.

The syntax is
SELECT * FROM (r NORMALIZE s USING() WITH(period_r, period_s)) c;

It currently is only implemented for empty USING clauses, and solely
int4range as range attributes.

Example:

A=# table r;

a | b | period_r

---+---+----------

a | B | [1,7)

b | B | [3,9)

c | G | [8,10)

(3 rows)

A=# table s;

c | d | period_s

---+---+----------

1 | B | [2,5)

2 | B | [3,4)

3 | B | [7,9)

(3 rows)

A=# SELECT * FROM (r NORMALIZE s USING() WITH(period_r, period_s)) c;

period_r | a | b

----------+---+---

[1,2) | a | B

[2,3) | a | B

[3,4) | a | B

[4,5) | a | B

[5,7) | a | B

[3,4) | b | B

[4,5) | b | B

[5,7) | b | B

[7,9) | b | B

(9 rows)

A=# EXPLAIN SELECT * FROM (r NORMALIZE s USING() WITH(period_r,
period_s)) c;
QUERY PLAN

--------------------------------------------------------------------------

Result (cost=2.15..2.22 rows=3 width=18)

-> Merge ??? Join (cost=2.15..2.23 rows=3 width=22)

Merge Cond: (r.period_r @> (range_split(s.period_s)))

-> Sort (cost=1.05..1.06 rows=3 width=18)

Sort Key: r.period_r

-> Seq Scan on r (cost=0.00..1.03 rows=3 width=18)

-> Sort (cost=1.09..1.10 rows=3 width=4)

Sort Key: (range_split(s.period_s)) USING <

-> ProjectSet (cost=0.00..1.07 rows=3 width=4)

-> Seq Scan on s (cost=0.00..1.03 rows=3
width=14)
(10 rows)

That is, we create a new join path within sort_inner_and_outer
(joinpath.c). First two projection nodes to extract the start- and
end-timepoints of the range type used as interval, and above an
append-plan to merge both subplans. In detail, outer_path is just sort,
whereas inner_path is append of (B, ts) projection with (B, te)
projection.

We changed this implementation and use a set-returning function called
"range_split", that extracts the upper and lower bound of a range and
returns two tuples. For instance, a tuple '[4,10),a' becomes two tuples
of the form '4,a' and '10,a'.

Hereby, B is a set of non-temporal attributes used in join equality
predicates, and [ts,te) forms the valid-time interval. Non-equality
predicates must be handled separately as a filter step.

The current prototype supports only an integer range-type without any
additional non-temporal attributes (empty USING clause).

Do you think, it is a good idea to extend the sort_inner_and_outer()
with a new branch, where jointype == NORMALIZE and add the projection
and append sub-paths there?

We actually extended sort_inner_and_outer now. It is an early solution,
to support discussions. Please see the two sections starting with "if
(jointype == JOIN_TEMPORAL_NORMALIZE)" inside sort_inner_and_outer:

The purpose of these sections is to change the inner path's range type
into its single bounds.

We accomplish this with a new function called range_split. We take the
inner clause and extract the second operator of an RANGE_EQ expression
out of it. We assume *for this prototype*, that their is only one such
operator and that it is solely used for NORMALIZE. Then, we replace it
with range_split. A range split returns a set of tuples, hence we add a
new "set projection path" above the inner path, and another sort path
above that.

What we like to discuss now is:
- Is sort_inner_and_outer the correct place to perform this split?
- How could we support OID_RANGE_ELEM_CONTAINED_OP for a NORMALIZE
mergejoin executor? If we use RANGE_ELEM_CONTAINED as operator, it is
not an equality operator, but if we use RANGE_EQ it assumes that the
right-hand-side of the operator must be a range as well.
- Should we better change our mergeclause to a RANGE_ELEM_CONTAINED
comparison, or keep RANGE_EQ and fix pathkeys later?
- How do we update equivalence classes, pathkeys, and any other struct,
when changing the inner relation's data type from "int4range" to "int"
in the query tree inside "sort_inner_and_outer" to get the correct
ordering and data types

Best regards,
Anton, Johann, Michael, Peter

Attachments:

tpg_normalize_v2.patchtext/x-patch; name=tpg_normalize_v2.patchDownload
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 5e52b90c00..ce4ffa992f 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -98,6 +98,102 @@
 #include "miscadmin.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "catalog/pg_operator.h"
+#include "nodes/nodeFuncs.h"
+#include "utils/fmgroids.h"
+#include "utils/rangetypes.h"
+#include "utils/typcache.h"
+#include "access/htup_details.h"                /* for heap_getattr */
+#include "nodes/print.h"                        /* for print_slot */
+#include "utils/datum.h"                        /* for datumCopy */
+
+
+// XXX PEMOSER ----------------------------
+// !!! THis is just for prototyping, delete asap...
+
+#define TEMPORAL_DEBUG
+/*
+ * #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
+
+TypeCacheEntry *testmytypcache;
+#define setSweepline(datum) \
+	node->sweepline = datumCopy(datum, node->datumFormat->attbyval, node->datumFormat->attlen)
+
+#define freeSweepline() \
+	if (! node->datumFormat->attbyval) pfree(DatumGetPointer(node->sweepline))
+
+ /*
+  * 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(TupleDescAttr(slot->tts_tupleDescriptor, 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(TupleDescAttr(slot->tts_tupleDescriptor,
+								attnum - 1)->attname),
+						attnum)));
+
+	return result;
+}
+// XXX PEMOSER ------------------------
+
 
 
 /*
@@ -138,6 +234,10 @@ typedef struct MergeJoinClauseData
 	 * stored here.
 	 */
 	SortSupportData ssup;
+
+	/* needed for Temporal Normalization */
+	bool             isnormalize;
+	TypeCacheEntry  *range_typcache;
 }			MergeJoinClauseData;
 
 /* Result type for MJEvalOuterValues and MJEvalInnerValues */
@@ -152,6 +252,57 @@ typedef enum
 #define MarkInnerTuple(innerTupleSlot, mergestate) \
 	ExecCopySlot((mergestate)->mj_MarkedTupleSlot, (innerTupleSlot))
 
+/*
+ * 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(MergeJoinState *mergestate,
+							 TupleTableSlot* slotToModify,
+							 TupleTableSlot* slotToStoreIn,
+							 Datum ts,
+							 Datum te,
+							 TypeCacheEntry *typcache)
+{
+	MemoryContext	oldContext;
+	HeapTuple		t;
+	RangeBound  	lower;
+	RangeBound  	upper;
+	bool        	empty = false;
+
+	/*
+	 * This should ideally be done with RangeBound types on the right-hand-side
+	 * created during range_split execution. Otherwise, we loose information about
+	 * inclusive/exclusive bounds and infinity. We would need to implement btree
+	 * operators for RangeBounds.
+	 */
+	lower.val = ts;
+	lower.lower = true;
+	lower.infinite = false;
+	lower.inclusive = true;
+
+	upper.val = te;
+	upper.lower = false;
+	upper.infinite = false;
+	upper.inclusive = false;
+
+	mergestate->newValues[0] = (Datum) make_range(typcache, &lower, &upper, empty);
+
+	oldContext = MemoryContextSwitchTo(mergestate->js.ps.ps_ResultTupleSlot->tts_mcxt);
+	t = heap_modify_tuple(slotToModify->tts_tuple,
+						  slotToModify->tts_tupleDescriptor,
+						  mergestate->newValues,
+						  mergestate->nullMask,
+						  mergestate->tsteMask);
+	MemoryContextSwitchTo(oldContext);
+	slotToStoreIn = ExecStoreTuple(t, slotToStoreIn, InvalidBuffer, true);
+
+	TPGdebug("Storing tuple:");
+	TPGdebugSlot(slotToStoreIn);
+}
+
 
 /*
  * MJExamineQuals
@@ -201,6 +352,8 @@ MJExamineQuals(List *mergeclauses,
 		Oid			op_righttype;
 		Oid			sortfunc;
 
+		pprint(qual);
+
 		if (!IsA(qual, OpExpr))
 			elog(ERROR, "mergejoin clause is not an OpExpr");
 
@@ -221,12 +374,31 @@ MJExamineQuals(List *mergeclauses,
 			elog(ERROR, "unsupported mergejoin strategy %d", opstrategy);
 		clause->ssup.ssup_nulls_first = nulls_first;
 
+		if (qual->opno == OID_RANGE_EQ_OP) {
+			Oid rngtypid;
+
+			// XXX PEMOSER Change opfamily and opfunc
+			qual->opfuncid = F_RANGE_CONTAINS; //<<--- opfuncid can be 0 during planning
+			qual->opno = OID_RANGE_CONTAINS_ELEM_OP; //OID_RANGE_CONTAINS_OP;
+			clause->isnormalize = true;
+
+			// Attention: cannot merge using non-equality operator 3890 <--- OID_RANGE_CONTAINS_OP
+			opfamily = 4103; //range_inclusion_ops from pg_opfamily.h
+
+			rngtypid = exprType((Node*)clause->lexpr->expr);
+			clause->range_typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
+			testmytypcache = clause->range_typcache;
+		} else {
+			clause->isnormalize = false;
+		}
+
+
 		/* Extract the operator's declared left/right datatypes */
 		get_op_opfamily_properties(qual->opno, opfamily, false,
 								   &op_strategy,
 								   &op_lefttype,
 								   &op_righttype);
-		if (op_strategy != BTEqualStrategyNumber)	/* should not happen */
+		if (op_strategy != BTEqualStrategyNumber && !clause->isnormalize)   /* should not happen */
 			elog(ERROR, "cannot merge using non-equality operator %u",
 				 qual->opno);
 
@@ -249,7 +421,7 @@ MJExamineQuals(List *mergeclauses,
 			/* The sort support function can provide a comparator */
 			OidFunctionCall1(sortfunc, PointerGetDatum(&clause->ssup));
 		}
-		if (clause->ssup.comparator == NULL)
+		if (clause->ssup.comparator == NULL && !clause->isnormalize)
 		{
 			/* support not available, get comparison func */
 			sortfunc = get_opfamily_proc(opfamily,
@@ -269,6 +441,77 @@ MJExamineQuals(List *mergeclauses,
 	return clauses;
 }
 
+static Datum
+getLower(Datum range, TypeCacheEntry *typcache)
+{
+	RangeBound  lower;
+	RangeBound  upper;
+	bool        empty;
+
+	range_deserialize(typcache, DatumGetRangeTypeP(range), &lower, &upper, &empty);
+
+	// XXX This is just a prototype function, we do not check for emptiness nor infinity yet...
+	// We will use RangeBounds in the future directly...
+	return lower.val;
+}
+
+static Datum
+getUpper(Datum range, TypeCacheEntry *typcache)
+{
+	RangeBound  lower;
+	RangeBound  upper;
+	bool        empty;
+
+	range_deserialize(typcache, DatumGetRangeTypeP(range), &lower, &upper, &empty);
+
+	// XXX This is just a prototype function, we do not check for emptiness nor infinity yet...
+	// We will use RangeBounds in the future directly...
+	return upper.val;
+}
+
+/*
+ * Return 0 if point is inside range, <0 if the point is right-of the second, or
+ * >0 if the point is left-of the range.
+ *
+ * This should ideally be done with RangeBound types on the right-hand-side
+ * created during range_split execution. Otherwise, we loose information about
+ * inclusive/exclusive bounds and infinity.
+ */
+static int
+ApplyNormalizeMatch(Datum ldatum, bool lisnull, Datum rdatum, bool risnull,
+					SortSupport ssup, TypeCacheEntry *typcache)
+{
+	RangeBound  lower;
+	RangeBound  upper;
+	bool        empty;
+	int32       result;
+
+	/* can't handle reverse sort order; should be prevented by optimizer */
+	Assert(!ssup->ssup_reverse);
+	Assert(!lisnull || !risnull);
+
+	if (lisnull)
+		return ssup->ssup_nulls_first ? -1 : 1;
+	if (risnull)
+		return ssup->ssup_nulls_first ? 1 : -1;
+
+	range_deserialize(typcache, DatumGetRangeTypeP(ldatum), &lower, &upper, &empty);
+
+	result = DatumGetInt32(FunctionCall2Coll(&typcache->rng_cmp_proc_finfo,
+											 typcache->rng_collation,
+											 lower.val, rdatum));
+	if (result == 1)
+		return 1;
+
+	result = DatumGetInt32(FunctionCall2Coll(&typcache->rng_cmp_proc_finfo,
+											 typcache->rng_collation,
+											 upper.val, rdatum));
+	if (result == 1)
+		return 0;
+
+	return -1;
+}
+
 /*
  * MJEvalOuterValues
  *
@@ -418,9 +661,19 @@ MJCompare(MergeJoinState *mergestate)
 			continue;
 		}
 
-		result = ApplySortComparator(clause->ldatum, clause->lisnull,
-									 clause->rdatum, clause->risnull,
-									 &clause->ssup);
+		if (clause->isnormalize)
+		{
+			result = ApplyNormalizeMatch(clause->ldatum, clause->lisnull,
+										 clause->rdatum, clause->risnull,
+										 &clause->ssup, clause->range_typcache);
+		}
+		else
+		{
+			result = ApplySortComparator(clause->ldatum, clause->lisnull,
+										 clause->rdatum, clause->risnull,
+										 &clause->ssup);
+		}
+
 
 		if (result != 0)
 			break;
@@ -611,6 +864,7 @@ ExecMergeJoin(PlanState *pstate)
 	ExprContext *econtext;
 	bool		doFillOuter;
 	bool		doFillInner;
+	TupleTableSlot *out = NULL;
 
 	CHECK_FOR_INTERRUPTS();
 
@@ -656,6 +910,13 @@ ExecMergeJoin(PlanState *pstate)
 				outerTupleSlot = ExecProcNode(outerPlan);
 				node->mj_OuterTupleSlot = outerTupleSlot;
 
+				/* XXX normalize (first call) */
+				if (node->mj_isNormalizer)
+				{
+					node->sameleft = true;
+					ExecCopySlot(node->prev, outerTupleSlot);
+				}
+
 				/* Compute join values and check for unmatchability */
 				switch (MJEvalOuterValues(node))
 				{
@@ -704,6 +965,18 @@ ExecMergeJoin(PlanState *pstate)
 				innerTupleSlot = ExecProcNode(innerPlan);
 				node->mj_InnerTupleSlot = innerTupleSlot;
 
+				/*
+				 * 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).
+				 */
+				if (node->mj_isNormalizer)
+				{
+					node->datumFormat = TupleDescAttr(innerTupleSlot->tts_tupleDescriptor, 0);
+					setSweepline(getLower(slotGetAttrNotNull(outerTupleSlot, 1), testmytypcache));
+					TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+				}
+
 				/* Compute join values and check for unmatchability */
 				switch (MJEvalInnerValues(node, innerTupleSlot))
 				{
@@ -789,6 +1062,10 @@ ExecMergeJoin(PlanState *pstate)
 				innerTupleSlot = node->mj_InnerTupleSlot;
 				econtext->ecxt_innertuple = innerTupleSlot;
 
+				TPGdebugSlot(outerTupleSlot);
+				TPGdebugSlot(node->prev);
+				TPGdebug("sameleft = %d", node->sameleft);
+
 				qualResult = (joinqual == NULL ||
 							  ExecQual(joinqual, econtext));
 				MJ_DEBUG_QUAL(joinqual, qualResult);
@@ -819,13 +1096,56 @@ ExecMergeJoin(PlanState *pstate)
 
 					if (qualResult)
 					{
+						TupleTableSlot *out;
+						bool            isNull;
+						Datum           currP1;
+
 						/*
 						 * qualification succeeded.  now form the desired
 						 * projection tuple and return the slot containing it.
 						 */
 						MJ_printf("ExecMergeJoin: returning tuple\n");
 
-						return ExecProject(node->js.ps.ps_ProjInfo);
+						out = ExecProject(node->js.ps.ps_ProjInfo);
+
+						if (!node->mj_isNormalizer)
+							return out;
+
+						if (node->sameleft)
+						{
+							currP1 = slot_getattr(innerTupleSlot, 1, &isNull);
+							TPGdebugDatum(currP1, node->datumFormat->atttypid);
+							if (node->sweepline < currP1)
+							{
+								temporalAdjustmentStoreTuple(node, outerTupleSlot, out, node->sweepline, currP1, testmytypcache);
+								freeSweepline();
+								setSweepline(currP1);
+
+								TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+								TPGdebugSlot(out);
+
+								return out;
+							}
+
+							ExecCopySlot(node->prev, outerTupleSlot);
+							node->mj_JoinState = EXEC_MJ_NEXTINNER;
+						}
+						else /* not node->sameleft */
+						{
+							Datum prevTe = getUpper(heapGetAttrNotNull(node->prev, 1), testmytypcache);
+
+							if (node->sweepline < prevTe)
+								temporalAdjustmentStoreTuple(node, node->prev, out, node->sweepline, prevTe, testmytypcache);
+
+							ExecCopySlot(node->prev, outerTupleSlot);
+							freeSweepline();
+							setSweepline(getLower(slotGetAttrNotNull(outerTupleSlot, 1), testmytypcache));
+							TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+							node->sameleft = true;
+							node->mj_JoinState = EXEC_MJ_NEXTINNER;
+							TPGdebugSlot(out);
+							return out;
+						}
 					}
 					else
 						InstrCountFiltered2(node, 1);
@@ -845,6 +1165,9 @@ ExecMergeJoin(PlanState *pstate)
 			case EXEC_MJ_NEXTINNER:
 				MJ_printf("ExecMergeJoin: EXEC_MJ_NEXTINNER\n");
 
+				if (node->mj_isNormalizer)
+					node->sameleft = true;
+
 				if (doFillInner && !node->mj_MatchedInner)
 				{
 					/*
@@ -947,6 +1270,9 @@ ExecMergeJoin(PlanState *pstate)
 			case EXEC_MJ_NEXTOUTER:
 				MJ_printf("ExecMergeJoin: EXEC_MJ_NEXTOUTER\n");
 
+				if (node->mj_isNormalizer)
+					node->sameleft = false;
+
 				if (doFillOuter && !node->mj_MatchedOuter)
 				{
 					/*
@@ -962,6 +1288,11 @@ ExecMergeJoin(PlanState *pstate)
 						return result;
 				}
 
+				// FIXME PEMOSER Only for normalizer...
+				TupleTableSlot *out = NULL;
+				if (node->mj_isNormalizer && !TupIsNull(innerTupleSlot))
+					out = ExecProject(node->js.ps.ps_ProjInfo);
+
 				/*
 				 * now we get the next outer tuple, if any
 				 */
@@ -994,6 +1325,19 @@ ExecMergeJoin(PlanState *pstate)
 							node->mj_JoinState = EXEC_MJ_ENDOUTER;
 							break;
 						}
+
+						if (node->mj_isNormalizer && !TupIsNull(node->prev) && !TupIsNull(innerTupleSlot))
+						{
+							MJ_printf("finalize normalizer!!!\n");
+							Datum prevTe = getUpper(heapGetAttrNotNull(node->prev, 1), testmytypcache);
+							TPGdebugDatum(prevTe, node->datumFormat->atttypid);
+							TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+							MJ_debugtup(node->prev);
+							temporalAdjustmentStoreTuple(node, node->prev, out, node->sweepline, prevTe, testmytypcache);
+							node->mj_JoinState = EXEC_MJ_ENDOUTER;
+							return out;
+						}
+
 						/* Otherwise we're done. */
 						return NULL;
 				}
@@ -1048,7 +1392,7 @@ ExecMergeJoin(PlanState *pstate)
 				compareResult = MJCompare(node);
 				MJ_DEBUG_COMPARE(compareResult);
 
-				if (compareResult == 0)
+				if (compareResult == 0 || (node->mj_isNormalizer && node->mj_markSet))
 				{
 					/*
 					 * the merge clause matched so now we restore the inner
@@ -1085,7 +1429,10 @@ ExecMergeJoin(PlanState *pstate)
 						/* we need not do MJEvalInnerValues again */
 					}
 
-					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
+					if (node->mj_isNormalizer)
+						node->mj_JoinState = EXEC_MJ_SKIP_TEST;
+					else
+						node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
 				{
@@ -1190,6 +1537,7 @@ ExecMergeJoin(PlanState *pstate)
 					MarkInnerTuple(node->mj_InnerTupleSlot, node);
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
+					node->mj_markSet = true;
 				}
 				else if (compareResult < 0)
 					node->mj_JoinState = EXEC_MJ_SKIPOUTER_ADVANCE;
@@ -1338,6 +1686,9 @@ ExecMergeJoin(PlanState *pstate)
 			case EXEC_MJ_ENDOUTER:
 				MJ_printf("ExecMergeJoin: EXEC_MJ_ENDOUTER\n");
 
+				if (node->mj_isNormalizer)
+					return NULL;
+
 				Assert(doFillInner);
 
 				if (!node->mj_MatchedInner)
@@ -1438,6 +1789,8 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	MergeJoinState *mergestate;
 	TupleDesc	outerDesc,
 				innerDesc;
+	int         numCols = list_length(node->join.plan.targetlist);
+
 
 	/* check for unsupported flags */
 	Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
@@ -1529,12 +1882,15 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 		ExecInitQual(node->join.joinqual, (PlanState *) mergestate);
 	/* mergeclauses are handled below */
 
+	mergestate->prev = ExecInitExtraTupleSlot(estate, outerDesc);
+
 	/*
 	 * detect whether we need only consider the first matching inner tuple
 	 */
 	mergestate->js.single_match = (node->join.inner_unique ||
 								   node->join.jointype == JOIN_SEMI);
 
+	mergestate->mj_isNormalizer = false;
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
@@ -1584,6 +1940,20 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("FULL JOIN is only supported with merge-joinable join conditions")));
 			break;
+		case JOIN_TEMPORAL_NORMALIZE:
+			mergestate->mj_FillOuter = false;
+			mergestate->mj_FillInner = false;
+			mergestate->mj_isNormalizer = true;
+
+			/* Init buffer values for heap_modify_tuple */
+			mergestate->newValues = palloc0(sizeof(Datum) * numCols);
+			mergestate->nullMask = palloc0(sizeof(bool) * numCols);
+			mergestate->tsteMask = palloc0(sizeof(bool) * numCols);
+
+			/* Not right??? -> Always the last in the list, since we add it during planning phase
+			 * XXX PEMOSER We need to find the correct position of "period" and set that here */
+			mergestate->tsteMask[/*numCols - 1*/0] = true;
+			break;
 		default:
 			elog(ERROR, "unrecognized join type: %d",
 				 (int) node->join.jointype);
diff --git a/src/backend/executor/nodeSort.c b/src/backend/executor/nodeSort.c
index 0d2acb665a..fcfd6f51b9 100644
--- a/src/backend/executor/nodeSort.c
+++ b/src/backend/executor/nodeSort.c
@@ -86,6 +86,12 @@ ExecSort(PlanState *pstate)
 		outerNode = outerPlanState(node);
 		tupDesc = ExecGetResultType(outerNode);
 
+		// XXX PEMOSER Manually fix sort operation of second attribute (former time, now upper(time))
+		// We must fix that in general... this is just a proof of concept brute-force solution!
+		if (plannode->plan.lefttree->type == T_ProjectSet) {
+			plannode->sortOperators[0] = 97; // 97 means "<" for int4, it was "<" for int4range
+		}
+
 		tuplesortstate = tuplesort_begin_heap(tupDesc,
 											  plannode->numCols,
 											  plannode->sortColIdx,
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 7bf67a0529..05a16bde0c 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -4543,6 +4543,7 @@ calc_joinrel_size_estimate(PlannerInfo *root,
 	switch (jointype)
 	{
 		case JOIN_INNER:
+		case JOIN_TEMPORAL_NORMALIZE:
 			nrows = outer_rows * inner_rows * fkselec * jselec;
 			/* pselec not used */
 			break;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 642f951093..ed779c2c93 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -22,6 +22,11 @@
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 #include "optimizer/planmain.h"
+#include "catalog/pg_operator.h"
+#include "optimizer/tlist.h"
+#include "utils/fmgroids.h"
+#include "nodes/makefuncs.h"
+#include "utils/lsyscache.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -195,7 +200,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	 * way of implementing a full outer join, so override enable_mergejoin if
 	 * it's a full join.
 	 */
-	if (enable_mergejoin || jointype == JOIN_FULL)
+	if (enable_mergejoin || jointype == JOIN_FULL || jointype == JOIN_TEMPORAL_NORMALIZE)
 		extra.mergeclause_list = select_mergejoin_clauses(root,
 														  joinrel,
 														  outerrel,
@@ -937,6 +942,80 @@ sort_inner_and_outer(PlannerInfo *root,
 		Assert(inner_path);
 		jointype = JOIN_INNER;
 	}
+	else if (jointype == JOIN_TEMPORAL_NORMALIZE)
+	{
+		/*
+		 * outer_path is just sort; inner_path is append of (B, ts) projection with (B, te) projection
+		 */
+		List *exprs = NIL; // to collect inner relation's targets
+		FuncExpr *f_split;
+		Var *innervar;
+
+		foreach(l, extra->mergeclause_list)
+		{
+			RestrictInfo       *rinfo = (RestrictInfo *) lfirst(l);
+			Expr               *clause  = (Expr *) rinfo->clause;
+
+			if (IsA(clause, OpExpr))
+			{
+				OpExpr *opexpr = (OpExpr *) clause;
+				if (opexpr->opno == OID_RANGE_EQ_OP)
+				{
+					if (IsA(lsecond(opexpr->args), Var)) {
+
+						// lsecond because it is from the second relation (=inner)
+						innervar = lsecond(opexpr->args);
+
+						f_split = makeFuncExpr(F_RANGE_SPLIT, 23, list_make1(innervar), 0, 0, 0);
+						f_split->funcretset = true;
+
+						/*
+						 * OUTER_VAR cannot be used here, because path creation does not know about it,
+						 * it will be introduced in plan creation.
+						 */
+						innervar = makeVar(2, innervar->varattno, f_split->funcresulttype, -1, 0, 0);
+					}
+				}
+				else
+				{
+					// lsecond because it is from the second relation (=inner)
+					exprs = lappend(exprs, lsecond(opexpr->args));
+				}
+			}
+		}
+
+		RestrictInfo       *rinfo = (RestrictInfo *) linitial(extra->mergeclause_list);
+		OpExpr *opexpr = (OpExpr *) rinfo->clause;
+		lsecond(opexpr->args) = f_split;
+		rinfo->right_em->em_expr = f_split;
+		rinfo->mergeopfamilies = get_mergejoin_opfamilies(opexpr->opno);
+
+		PathTarget *target_split = makeNode(PathTarget);
+		target_split->exprs = lappend(exprs, f_split);
+
+		set_pathtarget_cost_width(root, target_split);
+
+		inner_path = (Path *) create_set_projection_path(root, innerrel, inner_path, target_split);
+		innerrel->reltarget->exprs = inner_path->pathtarget->exprs;//list_make1(innervar);//copyObject(inner_path->pathtarget->exprs);
+		joinrel->reltarget->exprs = list_concat(copyObject(outerrel->reltarget->exprs), innerrel->reltarget->exprs);
+		set_pathtarget_cost_width(root, joinrel->reltarget);
+
+		innerrel->cheapest_total_path = inner_path;
+		innerrel->cheapest_startup_path = inner_path;
+		innerrel->cheapest_parameterized_paths = inner_path;
+		innerrel->pathlist = list_make1(inner_path);
+
+		extra->sjinfo->semi_rhs_exprs = list_make1(f_split);
+		extra->sjinfo->semi_operators = NIL;
+		extra->sjinfo->semi_operators = lappend_oid(extra->sjinfo->semi_operators, 96);
+
+		Assert(inner_path);
+
+		innerrel->cheapest_total_path = inner_path;
+		innerrel->cheapest_startup_path = inner_path;
+		innerrel->cheapest_parameterized_paths = inner_path;
+	}
+
 
 	/*
 	 * If the joinrel is parallel-safe, we may be able to consider a partial
@@ -1028,6 +1107,16 @@ sort_inner_and_outer(PlannerInfo *root,
 		merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
 											 outerkeys);
 
+		if (jointype == JOIN_TEMPORAL_NORMALIZE)
+		{
+			inner_path = (Path *) create_sort_path(root, innerrel, inner_path, innerkeys, -1);
+			innerrel->cheapest_total_path = inner_path;
+			innerrel->cheapest_startup_path = inner_path;
+			innerrel->cheapest_parameterized_paths = inner_path;
+			innerrel->pathlist = list_make1(inner_path);
+			Assert(inner_path);
+		}
+
 		/*
 		 * And now we can make the path.
 		 *
@@ -1360,6 +1449,7 @@ match_unsorted_outer(PlannerInfo *root,
 			break;
 		case JOIN_RIGHT:
 		case JOIN_FULL:
+		case JOIN_TEMPORAL_NORMALIZE:
 			nestjoinOK = false;
 			useallclauses = true;
 			break;
@@ -1686,6 +1776,10 @@ hash_inner_and_outer(PlannerInfo *root,
 	List	   *hashclauses;
 	ListCell   *l;
 
+	/* Hashjoin is not allowed for temporal NORMALIZE */
+	if (jointype == JOIN_TEMPORAL_NORMALIZE)
+		return;
+
 	/*
 	 * We need to build only one hashclauses list for any given pair of outer
 	 * and inner relations; all of the hashable clauses will be used as keys.
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index d3d21fed5d..2dfbcafff3 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -898,6 +898,21 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1,
 								 JOIN_ANTI, sjinfo,
 								 restrictlist);
 			break;
+		case JOIN_TEMPORAL_NORMALIZE:
+			if (is_dummy_rel(rel1) || is_dummy_rel(rel2) ||
+				restriction_is_constant_false(restrictlist, joinrel, false))
+			{
+				mark_dummy_rel(joinrel);
+				break;
+			}
+
+			/*
+			 * Temporal normalization does not support re-ordering of rels
+			 */
+			add_paths_to_joinrel(root, joinrel, rel1, rel2,
+								 JOIN_TEMPORAL_NORMALIZE, sjinfo,
+								 restrictlist);
+			break;
 		default:
 			/* other values not expected here */
 			elog(ERROR, "unrecognized join type: %d", (int) sjinfo->jointype);
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index ec66cb9c3c..334073b0fb 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -833,7 +833,12 @@ build_join_pathkeys(PlannerInfo *root,
 					JoinType jointype,
 					List *outer_pathkeys)
 {
-	if (jointype == JOIN_FULL || jointype == JOIN_RIGHT)
+	/*
+	 * TEMPORAL NORMALIZE: To improve this, we would need to remove only
+	 * temporal range types from the path key list, not all
+	 */
+	if (jointype == JOIN_FULL || jointype == JOIN_RIGHT
+			|| jointype == JOIN_TEMPORAL_NORMALIZE)
 		return NIL;
 
 	/*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ae41c9efa0..ef306c7c59 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -4107,6 +4107,15 @@ create_mergejoin_plan(PlannerInfo *root,
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	/*
+	 * XXX PEMOSER NORMALIZE needs a result node above to properly
+	 * handle target lists, functions and constants
+	 * Maybe we need to refine this like in create_unique_plan:
+	 * "If the top plan node can't do projections..."
+	 */
+	if (best_path->jpath.jointype == JOIN_TEMPORAL_NORMALIZE)
+		join_plan = make_result(tlist, NULL, join_plan);
+
 	return join_plan;
 }
 
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 01335db511..aa6298886a 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -31,6 +31,7 @@
 #include "parser/analyze.h"
 #include "rewrite/rewriteManip.h"
 #include "utils/lsyscache.h"
+#include "catalog/pg_operator.h"
 
 
 /* These parameters are set by GUC */
@@ -896,6 +897,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
 		switch (j->jointype)
 		{
 			case JOIN_INNER:
+			case JOIN_TEMPORAL_NORMALIZE:
 				leftjoinlist = deconstruct_recurse(root, j->larg,
 												   below_outer_join,
 												   &leftids, &left_inners,
@@ -1018,7 +1020,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
 										*inner_join_rels,
 										j->jointype,
 										my_quals);
-			if (j->jointype == JOIN_SEMI)
+			if (j->jointype == JOIN_SEMI || j->jointype == JOIN_TEMPORAL_NORMALIZE)
 				ojscope = NULL;
 			else
 				ojscope = bms_union(sjinfo->min_lefthand,
@@ -1437,7 +1439,8 @@ compute_semijoin_info(SpecialJoinInfo *sjinfo, List *clause)
 	sjinfo->semi_rhs_exprs = NIL;
 
 	/* Nothing more to do if it's not a semijoin */
-	if (sjinfo->jointype != JOIN_SEMI)
+	if (sjinfo->jointype != JOIN_SEMI
+			&& sjinfo->jointype != JOIN_TEMPORAL_NORMALIZE)
 		return;
 
 	/*
@@ -2621,6 +2624,10 @@ check_mergejoinable(RestrictInfo *restrictinfo)
 	opno = ((OpExpr *) clause)->opno;
 	leftarg = linitial(((OpExpr *) clause)->args);
 
+	// XXX PEMOSER Hardcoded NORMALIZE detection... change this. Read the note below...
+	if (opno == OID_RANGE_EQ_OP)
+		restrictinfo->temp_normalizer = true;
+
 	if (op_mergejoinable(opno, exprType(leftarg)) &&
 		!contain_volatile_functions((Node *) clause))
 		restrictinfo->mergeopfamilies = get_mergejoin_opfamilies(opno);
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index c3f46a26c3..81a3152de8 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -785,6 +785,7 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 		switch (j->jointype)
 		{
 			case JOIN_INNER:
+			case JOIN_TEMPORAL_NORMALIZE:
 
 				/*
 				 * INNER JOIN can allow deletion of either child node, but not
diff --git a/src/backend/optimizer/util/restrictinfo.c b/src/backend/optimizer/util/restrictinfo.c
index edf5a4807f..45d573eccd 100644
--- a/src/backend/optimizer/util/restrictinfo.c
+++ b/src/backend/optimizer/util/restrictinfo.c
@@ -186,6 +186,7 @@ make_restrictinfo_internal(Expr *clause,
 	restrictinfo->outer_selec = -1;
 
 	restrictinfo->mergeopfamilies = NIL;
+	restrictinfo->temp_normalizer = false;
 
 	restrictinfo->left_ec = NULL;
 	restrictinfo->right_ec = NULL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4bd2223f26..3aecacfe1c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -432,6 +432,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	join_outer join_qual
 %type <jtype>	join_type
 
+%type <node>    normalizer_qual
+%type <jexpr>   normalized_table
+%type <list>	temporal_bounds temporal_bounds_list
+
 %type <list>	extract_list overlay_list position_list
 %type <list>	substr_list trim_list
 %type <list>	opt_interval interval_second
@@ -654,7 +658,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
 
@@ -11921,6 +11925,11 @@ table_ref:	relation_expr opt_alias_clause
 					$2->alias = $4;
 					$$ = (Node *) $2;
 				}
+			| '(' normalized_table ')' alias_clause
+				{
+					$2->alias = $4;
+					$$ = (Node *) $2;
+				}
 		;
 
 
@@ -12039,6 +12048,59 @@ opt_alias_clause: alias_clause						{ $$ = $1; }
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+/*
+ * Temporal alignment statements
+ */
+temporal_bounds: WITH '(' temporal_bounds_list ')'				{ $$ = $3; }
+		;
+
+temporal_bounds_list:
+			columnref
+				{
+					$$ = list_make1($1);
+				}
+			| temporal_bounds_list ',' columnref
+				{
+					$$ = lappend($1, $3);
+				}
+		;
+
+normalizer_qual:
+			USING '(' name_list ')'					{ $$ = (Node *) $3; }
+			| USING '(' ')'							{ $$ = (Node *) NIL; }
+		;
+
+normalized_table:
+			table_ref NORMALIZE table_ref normalizer_qual temporal_bounds
+				{
+					JoinExpr *n = makeNode(JoinExpr);
+					n->jointype = JOIN_TEMPORAL_NORMALIZE;
+					n->isNatural = false;
+					n->larg = $1;
+					n->rarg = $3;
+
+					n->usingClause = NIL;
+
+					if ($4 != NULL && IsA($4, List))
+						n->usingClause = (List *) $4; /* USING clause */
+
+					/*
+					 * A list for our valid-time boundaries with two range typed
+					 * values.
+					 */
+					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;
+				}
+		;
+
 /*
  * func_alias_clause can include both an Alias and a coldeflist, so we make it
  * return a 2-element list that gets disassembled by calling production.
@@ -15451,6 +15513,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 cfd4b91897..ff2020f724 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -62,7 +62,7 @@ static void extractRemainingColumns(List *common_colnames,
 						List **res_colnames, List **res_colvars);
 static Node *transformJoinUsingClause(ParseState *pstate,
 						 RangeTblEntry *leftRTE, RangeTblEntry *rightRTE,
-						 List *leftVars, List *rightVars);
+						 List *leftVars, List *rightVars, List *normalizeVars);
 static Node *transformJoinOnClause(ParseState *pstate, JoinExpr *j,
 					  List *namespace);
 static RangeTblEntry *getRTEForSpecialRelationTypes(ParseState *pstate,
@@ -339,7 +339,7 @@ extractRemainingColumns(List *common_colnames,
 static Node *
 transformJoinUsingClause(ParseState *pstate,
 						 RangeTblEntry *leftRTE, RangeTblEntry *rightRTE,
-						 List *leftVars, List *rightVars)
+						 List *leftVars, List *rightVars, List *normalizeVars)
 {
 	Node	   *result;
 	List	   *andargs = NIL;
@@ -373,6 +373,17 @@ transformJoinUsingClause(ParseState *pstate,
 		andargs = lappend(andargs, e);
 	}
 
+	/* Temporal NORMALIZE appends an expression to compare temporal bounds */
+	if (normalizeVars)
+	{
+		A_Expr *e;
+		e = makeSimpleA_Expr(AEXPR_OP, "=",
+							 (Node *) copyObject(linitial(normalizeVars)),
+							 (Node *) copyObject(lsecond(normalizeVars)),
+							 -1);
+		andargs = lappend(andargs, e);
+	}
+
 	/* Only need an AND if there's more than one join column */
 	if (list_length(andargs) == 1)
 		result = (Node *) linitial(andargs);
@@ -1232,6 +1243,7 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		int			sv_namespace_length;
 		RangeTblEntry *rte;
 		int			k;
+		int         isNormalize = (j->jointype == JOIN_TEMPORAL_NORMALIZE);
 
 		/*
 		 * Recursively process the left subtree, then the right.  We must do
@@ -1342,7 +1354,8 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		res_colnames = NIL;
 		res_colvars = NIL;
 
-		if (j->usingClause)
+		/* NORMALIZE supports empty using clauses */
+		if (j->usingClause || (isNormalize && j->usingClause == NIL))
 		{
 			/*
 			 * JOIN/USING (or NATURAL JOIN, as transformed above). Transform
@@ -1352,6 +1365,7 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 			List	   *ucols = j->usingClause;
 			List	   *l_usingvars = NIL;
 			List	   *r_usingvars = NIL;
+			List       *normalize_vars = NIL;
 			ListCell   *ucol;
 
 			Assert(j->quals == NULL);	/* shouldn't have ON() too */
@@ -1437,11 +1451,90 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 														 r_colvar));
 			}
 
+			/* Only for temporal NORMALIZE */
+			if (isNormalize)
+			{
+				int         ndx = 0;
+				ListCell   *col;
+				Var        *l_boundvar;
+				Var        *r_boundvar;
+
+				int         l_bound_index = -1;
+				int         r_bound_index = -1;
+				char       *l_bound;
+				char       *r_bound;
+				ListCell *lboundcol = linitial(((ColumnRef *)linitial(j->temporalBounds))->fields);
+				ListCell *rboundcol = linitial(((ColumnRef *)lsecond(j->temporalBounds))->fields);
+
+				l_bound = strVal(lboundcol);
+				r_bound = strVal(rboundcol);
+
+				/* Find the first bound in left input */
+				foreach(col, l_colnames)
+				{
+					char       *l_colname = strVal(lfirst(col));
+
+					if (strcmp(l_colname, l_bound) == 0)
+					{
+						if (l_bound_index >= 0)
+							ereport(ERROR,
+									(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+											 errmsg("temporal bound name \"%s\" appears more than once in left table",
+													l_bound)));
+						l_bound_index = ndx;
+					}
+					ndx++;
+				}
+
+				if (l_bound_index < 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_COLUMN),
+									 errmsg("column \"%s\" specified in normalizer's WITH clause does not exist in left table",
+											l_bound)));
+
+				/* Find the second bound in right input */
+				ndx = 0;
+				foreach(col, r_colnames)
+				{
+					char       *r_colname = strVal(lfirst(col));
+
+					if (strcmp(r_colname, r_bound) == 0)
+					{
+						if (r_bound_index >= 0)
+							ereport(ERROR,
+									(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+											 errmsg("temporal bound name \"%s\" appears more than once in right table",
+													l_bound)));
+						r_bound_index = ndx;
+					}
+					ndx++;
+				}
+
+				if (r_bound_index < 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_COLUMN),
+									 errmsg("column \"%s\" specified in normalizer's WITH clause does not exist in right table",
+											r_bound)));
+
+				l_boundvar = list_nth(l_colvars, l_bound_index);
+				normalize_vars = lappend(normalize_vars, l_boundvar);
+				r_boundvar = list_nth(r_colvars, r_bound_index);
+				normalize_vars = lappend(normalize_vars, r_boundvar);
+
+				res_colnames = lappend(res_colnames, lboundcol);
+				res_colvars = lappend(res_colvars,
+									  buildMergedJoinVar(pstate,
+														 j->jointype,
+														 l_boundvar,
+														 r_boundvar));
+			}
+
 			j->quals = transformJoinUsingClause(pstate,
 												l_rte,
 												r_rte,
 												l_usingvars,
-												r_usingvars);
+												r_usingvars,
+												normalize_vars);
 		}
 		else if (j->quals)
 		{
@@ -1457,13 +1550,21 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		extractRemainingColumns(res_colnames,
 								l_colnames, l_colvars,
 								&l_colnames, &l_colvars);
-		extractRemainingColumns(res_colnames,
-								r_colnames, r_colvars,
-								&r_colnames, &r_colvars);
+
+		//Temporal normalizers expose only outer relation's columns...
+		if (!isNormalize)
+			extractRemainingColumns(res_colnames,
+									r_colnames, r_colvars,
+									&r_colnames, &r_colvars);
+
 		res_colnames = list_concat(res_colnames, l_colnames);
 		res_colvars = list_concat(res_colvars, l_colvars);
-		res_colnames = list_concat(res_colnames, r_colnames);
-		res_colvars = list_concat(res_colvars, r_colvars);
+
+		if (!isNormalize)
+		{
+			res_colnames = list_concat(res_colnames, r_colnames);
+			res_colvars = list_concat(res_colvars, r_colvars);
+		}
 
 		/*
 		 * Check alias (AS clause), if any.
@@ -1606,6 +1707,7 @@ buildMergedJoinVar(ParseState *pstate, JoinType jointype,
 	switch (jointype)
 	{
 		case JOIN_INNER:
+		case JOIN_TEMPORAL_NORMALIZE:
 
 			/*
 			 * We can use either var; prefer non-coerced one if available.
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index 5a5d0a0b8f..2ca5ba52cc 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -40,6 +40,7 @@
 #include "utils/lsyscache.h"
 #include "utils/rangetypes.h"
 #include "utils/timestamp.h"
+#include "funcapi.h"
 
 
 #define RANGE_EMPTY_LITERAL "empty"
@@ -466,6 +467,79 @@ range_upper(PG_FUNCTION_ARGS)
 	PG_RETURN_DATUM(upper.val);
 }
 
+/* split lower/upper bound into two rows of data */
+Datum
+range_split(PG_FUNCTION_ARGS)
+{
+	typedef struct
+	{
+		RangeBound	lower;
+		RangeBound	upper;
+		bool		empty;
+	} RangeSplitFuncContext;
+
+	FuncCallContext *funcctx;
+	MemoryContext oldcontext;
+	RangeSplitFuncContext *fctx;
+
+	/* stuff done only on the first call of the function */
+	if (SRF_IS_FIRSTCALL())
+	{
+		RangeType  		*r1;
+		TypeCacheEntry  *typcache;
+
+		/* create a function context for cross-call persistence */
+		funcctx = SRF_FIRSTCALL_INIT();
+
+		/*
+		 * switch to memory context appropriate for multiple function calls
+		 */
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		r1 = PG_GETARG_RANGE_P(0);
+
+		/* allocate memory for user context */
+		fctx = (RangeSplitFuncContext *) palloc(sizeof(RangeSplitFuncContext));
+
+		/*
+		 * We cannot use range_get_typecache, because it would overwrite
+		 * fcinfo->flinfo->fn_extra
+		 */
+		typcache = lookup_type_cache(RangeTypeGetOid(r1), TYPECACHE_RANGE_INFO);
+		if (typcache->rngelemtype == NULL)
+			elog(ERROR, "type %u is not a range type", RangeTypeGetOid(r1));
+		range_deserialize(typcache, r1, &fctx->lower, &fctx->upper, &fctx->empty);
+
+		funcctx->user_fctx = fctx;
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	/* stuff done on every call of the function */
+	funcctx = SRF_PERCALL_SETUP();
+	fctx = (RangeSplitFuncContext *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr == 0)
+	{
+		/* Return NULL if there's no finite lower bound */
+		if (fctx->empty || fctx->lower.infinite)
+			SRF_RETURN_NEXT_NULL(funcctx);
+
+		SRF_RETURN_NEXT(funcctx, fctx->lower.val);
+	}
+
+	if (funcctx->call_cntr == 1)
+	{
+		/* Return NULL if there's no finite upper bound */
+		if (fctx->empty || fctx->upper.infinite)
+			SRF_RETURN_NEXT_NULL(funcctx);
+
+		SRF_RETURN_NEXT(funcctx, fctx->upper.val);
+	}
+
+	/* done, after extracting lower and upper bounds */
+	SRF_RETURN_DONE(funcctx);
+}
+
 
 /* range -> bool functions */
 
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index f1c78ffb65..2280d0c9a8 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -2304,6 +2304,7 @@ eqjoinsel(PG_FUNCTION_ARGS)
 		case JOIN_INNER:
 		case JOIN_LEFT:
 		case JOIN_FULL:
+		case JOIN_TEMPORAL_NORMALIZE:
 			selec = eqjoinsel_inner(operator, &vardata1, &vardata2);
 			break;
 		case JOIN_SEMI:
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index d9b6bad614..d08b87fd18 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3141,7 +3141,7 @@
   oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' },
 
 # generic range type operators
-{ oid => '3882', descr => 'equal',
+{ oid => '3882', descr => 'equal', oid_symbol => 'OID_RANGE_EQ_OP',
   oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'anyrange',
   oprright => 'anyrange', oprresult => 'bool', oprcom => '=(anyrange,anyrange)',
   oprnegate => '<>(anyrange,anyrange)', oprcode => 'range_eq',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 860571440a..cc88b5dfc0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9538,6 +9538,10 @@
 { oid => '3867',
   proname => 'range_union', prorettype => 'anyrange',
   proargtypes => 'anyrange anyrange', prosrc => 'range_union' },
+{ oid => '3996',
+  descr => 'lower and upper bound of range returned as two tuples',
+  proname => 'range_split', prorettype => 'anyelement', proretset => 't',
+  proargtypes => 'anyrange', prosrc => 'range_split' },
 { oid => '4057',
   descr => 'the smallest range which includes both of the given ranges',
   proname => 'range_merge', prorettype => 'anyrange',
diff --git a/src/include/executor/execdebug.h b/src/include/executor/execdebug.h
index 236b2cc4fd..a952b80d7f 100644
--- a/src/include/executor/execdebug.h
+++ b/src/include/executor/execdebug.h
@@ -82,6 +82,7 @@
  *		sort node debugging defines
  * ----------------
  */
+#define EXEC_SORTDEBUG
 #ifdef EXEC_SORTDEBUG
 #define SO_nodeDisplay(l)				nodeDisplay(l)
 #define SO_printf(s)					printf(s)
@@ -96,14 +97,15 @@
  *		merge join debugging defines
  * ----------------
  */
+#define EXEC_MERGEJOINDEBUG
 #ifdef EXEC_MERGEJOINDEBUG
 
 #define MJ_nodeDisplay(l)				nodeDisplay(l)
-#define MJ_printf(s)					printf(s)
-#define MJ1_printf(s, p)				printf(s, p)
-#define MJ2_printf(s, p1, p2)			printf(s, p1, p2)
-#define MJ_debugtup(slot)				debugtup(slot, NULL)
-#define MJ_dump(state)					ExecMergeTupleDump(state)
+#define MJ_printf(s)					printf(s); fflush(stdout)
+#define MJ1_printf(s, p)				printf(s, p); fflush(stdout)
+#define MJ2_printf(s, p1, p2)			printf(s, p1, p2); fflush(stdout)
+#define MJ_debugtup(slot)				debugtup(slot, NULL); fflush(stdout)
+#define MJ_dump(state)					ExecMergeTupleDump(state); fflush(stdout)
 #define MJ_DEBUG_COMPARE(res) \
   MJ1_printf("  MJCompare() returns %d\n", (res))
 #define MJ_DEBUG_QUAL(clause, res) \
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index c830f141b1..ec23870260 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1762,6 +1762,17 @@ typedef struct MergeJoinState
 	TupleTableSlot *mj_NullInnerTupleSlot;
 	ExprContext *mj_OuterEContext;
 	ExprContext *mj_InnerEContext;
+
+	/* needed by temporal normalization */
+	bool				 mj_markSet;
+	bool				 mj_isNormalizer;
+	bool				 sameleft;
+	bool				*nullMask;		/* See heap_modify_tuple */
+	bool				*tsteMask;		/* See heap_modify_tuple */
+	Datum				*newValues;		/* tuple values that get updated */
+	Datum				 sweepline;
+	Form_pg_attribute	 datumFormat;	/* Datum format of sweepline, P1, P2 */
+	TupleTableSlot 		*prev;
 } MergeJoinState;
 
 /* ----------------
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 697d3d7a5f..e784a2f5ed 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -84,6 +84,7 @@ typedef enum NodeTag
 	T_SetOp,
 	T_LockRows,
 	T_Limit,
+	T_TemporalAdjustment,
 	/* these aren't subclasses of Plan: */
 	T_NestLoopParam,
 	T_PlanRowMark,
@@ -140,6 +141,7 @@ typedef enum NodeTag
 	T_SetOpState,
 	T_LockRowsState,
 	T_LimitState,
+	T_TemporalAdjustmentState,
 
 	/*
 	 * TAGS FOR PRIMITIVE NODES (primnodes.h)
@@ -256,6 +258,7 @@ typedef enum NodeTag
 	T_LockRowsPath,
 	T_ModifyTablePath,
 	T_LimitPath,
+	T_TemporalAdjustmentPath,
 	/* these aren't subclasses of Path: */
 	T_EquivalenceClass,
 	T_EquivalenceMember,
@@ -475,6 +478,7 @@ typedef enum NodeTag
 	T_PartitionRangeDatum,
 	T_PartitionCmd,
 	T_VacuumRelation,
+	T_TemporalClause,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
@@ -706,7 +710,12 @@ 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
+	 */
+	JOIN_TEMPORAL_NORMALIZE
 
 	/*
 	 * We might need additional join types someday.
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 1b4b0d75af..41cc8e395d 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -501,6 +501,7 @@ typedef struct OpExpr
 	Oid			inputcollid;	/* OID of collation that operator should use */
 	List	   *args;			/* arguments to the operator (1 or 2) */
 	int			location;		/* token location, or -1 if unknown */
+	bool		isnormalize;
 } OpExpr;
 
 /*
@@ -1461,6 +1462,8 @@ 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 */
 } JoinExpr;
 
 /*----------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index adb4265047..de65ecf19a 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1945,6 +1945,7 @@ typedef struct RestrictInfo
 
 	/* valid if clause is hashjoinable, else InvalidOid: */
 	Oid			hashjoinoperator;	/* copy of clause operator */
+	bool        temp_normalizer;
 
 	/* cache space for hashclause processing; -1 if not yet set */
 	Selectivity left_bucketsize;	/* avg bucketsize of left side */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 23db40147b..872dd0f0f9 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -262,6 +262,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)
#59Peter Moser
pitiz29a@gmail.com
In reply to: Peter Moser (#58)
1 attachment(s)
Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

Dear all,
we rebased our temporal normalization patch on top of 554ebf687852d045f0418d3242b978b49f160f44 from 2019-02-28.

On 9/7/18 1:02 PM, Peter Moser wrote:

The syntax is
SELECT * FROM (r NORMALIZE s USING() WITH(period_r, period_s)) c;

Please find all information about our decisions and current state within the previous email.

What we like to discuss now is:
- Is sort_inner_and_outer the correct place to perform this split?
- How could we support OID_RANGE_ELEM_CONTAINED_OP for a NORMALIZE
  mergejoin executor? If we use RANGE_ELEM_CONTAINED as operator, it is
  not an equality operator, but if we use RANGE_EQ it assumes that the
  right-hand-side of the operator must be a range as well.
- Should we better change our mergeclause to a RANGE_ELEM_CONTAINED
  comparison, or keep RANGE_EQ and fix pathkeys later?
- How do we update equivalence classes, and pathkeys
  when changing the inner relation's data type from "int4range" to "int"
  in the query tree inside "sort_inner_and_outer" to get the correct
  ordering and data types

I will also add this prototype (WIP) patch to the commitfest of March, as suggested by two developers met at the
FOSDEM some weeks ago.

Best regards,
Anton, Johann, Michael, Peter

Attachments:

tpg_normalize_v3.patchtext/x-patch; name=tpg_normalize_v3.patchDownload
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 2a1d000b03..a309596fa1 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -99,6 +99,106 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+// XXX TEMPORAL NORMALIZE PEMOSER ----------------------------
+// !!! THis is just for prototyping, delete asap...
+
+#include "catalog/pg_operator.h"
+#include "nodes/nodeFuncs.h"
+#include "utils/fmgroids.h"
+#include "utils/rangetypes.h"
+#include "utils/typcache.h"
+#include "access/htup_details.h"                /* for heap_getattr */
+#include "nodes/print.h"                        /* for print_slot */
+#include "utils/datum.h"                        /* for datumCopy */
+
+
+
+#define TEMPORAL_DEBUG
+/*
+ * #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
+
+TypeCacheEntry *testmytypcache;
+#define setSweepline(datum) \
+	node->sweepline = datumCopy(datum, node->datumFormat->attbyval, node->datumFormat->attlen)
+
+#define freeSweepline() \
+	if (! node->datumFormat->attbyval) pfree(DatumGetPointer(node->sweepline))
+
+ /*
+  * 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(TupleDescAttr(slot->tts_tupleDescriptor, 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;
+	HeapTuple tuple;
+
+	tuple = ExecFetchSlotHeapTuple(slot, true, NULL);
+	result = heap_getattr(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(TupleDescAttr(slot->tts_tupleDescriptor,
+								attnum - 1)->attname),
+						attnum)));
+
+	return result;
+}
+
+// XXX TEMPORAL NORMALIZE PEMOSER END ------------------------
+
 
 /*
  * States of the ExecMergeJoin state machine
@@ -138,6 +238,10 @@ typedef struct MergeJoinClauseData
 	 * stored here.
 	 */
 	SortSupportData ssup;
+
+	/* needed for Temporal Normalization */
+	bool             isnormalize;
+	TypeCacheEntry  *range_typcache;
 }			MergeJoinClauseData;
 
 /* Result type for MJEvalOuterValues and MJEvalInnerValues */
@@ -152,6 +256,59 @@ typedef enum
 #define MarkInnerTuple(innerTupleSlot, mergestate) \
 	ExecCopySlot((mergestate)->mj_MarkedTupleSlot, (innerTupleSlot))
 
+/*
+ * 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(MergeJoinState *mergestate,
+							 TupleTableSlot* slotToModify,
+							 TupleTableSlot* slotToStoreIn,
+							 Datum ts,
+							 Datum te,
+							 TypeCacheEntry *typcache)
+{
+	MemoryContext	oldContext;
+	HeapTuple		t;
+	RangeBound  	lower;
+	RangeBound  	upper;
+	bool        	empty = false;
+	HeapTuple       tuple;
+
+	/*
+	 * This should ideally be done with RangeBound types on the right-hand-side
+	 * created during range_split execution. Otherwise, we loose information about
+	 * inclusive/exclusive bounds and infinity. We would need to implement btree
+	 * operators for RangeBounds.
+	 */
+	lower.val = ts;
+	lower.lower = true;
+	lower.infinite = false;
+	lower.inclusive = true;
+
+	upper.val = te;
+	upper.lower = false;
+	upper.infinite = false;
+	upper.inclusive = false;
+
+	mergestate->newValues[0] = (Datum) make_range(typcache, &lower, &upper, empty);
+
+	oldContext = MemoryContextSwitchTo(mergestate->js.ps.ps_ResultTupleSlot->tts_mcxt);
+	tuple = ExecFetchSlotHeapTuple(slotToModify, true, NULL);
+	t = heap_modify_tuple(tuple,
+						  slotToModify->tts_tupleDescriptor,
+						  mergestate->newValues,
+						  mergestate->nullMask,
+						  mergestate->tsteMask);
+	MemoryContextSwitchTo(oldContext);
+	ExecForceStoreHeapTuple(t, slotToStoreIn);
+
+	TPGdebug("Storing tuple:");
+	TPGdebugSlot(slotToStoreIn);
+}
+
 
 /*
  * MJExamineQuals
@@ -201,6 +358,8 @@ MJExamineQuals(List *mergeclauses,
 		Oid			op_righttype;
 		Oid			sortfunc;
 
+		pprint(qual);
+
 		if (!IsA(qual, OpExpr))
 			elog(ERROR, "mergejoin clause is not an OpExpr");
 
@@ -221,12 +380,31 @@ MJExamineQuals(List *mergeclauses,
 			elog(ERROR, "unsupported mergejoin strategy %d", opstrategy);
 		clause->ssup.ssup_nulls_first = nulls_first;
 
+		if (qual->opno == OID_RANGE_EQ_OP) {
+			Oid rngtypid;
+
+			// XXX PEMOSER Change opfamily and opfunc
+			qual->opfuncid = F_RANGE_CONTAINS; //<<--- opfuncid can be 0 during planning
+			qual->opno = OID_RANGE_CONTAINS_ELEM_OP; //OID_RANGE_CONTAINS_OP;
+			clause->isnormalize = true;
+
+			// Attention: cannot merge using non-equality operator 3890 <--- OID_RANGE_CONTAINS_OP
+			opfamily = 4103; //range_inclusion_ops from pg_opfamily.h
+
+			rngtypid = exprType((Node*)clause->lexpr->expr);
+			clause->range_typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
+			testmytypcache = clause->range_typcache;
+		} else {
+			clause->isnormalize = false;
+		}
+
+
 		/* Extract the operator's declared left/right datatypes */
 		get_op_opfamily_properties(qual->opno, opfamily, false,
 								   &op_strategy,
 								   &op_lefttype,
 								   &op_righttype);
-		if (op_strategy != BTEqualStrategyNumber)	/* should not happen */
+		if (op_strategy != BTEqualStrategyNumber && !clause->isnormalize)   /* should not happen */
 			elog(ERROR, "cannot merge using non-equality operator %u",
 				 qual->opno);
 
@@ -249,7 +427,7 @@ MJExamineQuals(List *mergeclauses,
 			/* The sort support function can provide a comparator */
 			OidFunctionCall1(sortfunc, PointerGetDatum(&clause->ssup));
 		}
-		if (clause->ssup.comparator == NULL)
+		if (clause->ssup.comparator == NULL && !clause->isnormalize)
 		{
 			/* support not available, get comparison func */
 			sortfunc = get_opfamily_proc(opfamily,
@@ -269,6 +447,77 @@ MJExamineQuals(List *mergeclauses,
 	return clauses;
 }
 
+static Datum
+getLower(Datum range, TypeCacheEntry *typcache)
+{
+	RangeBound  lower;
+	RangeBound  upper;
+	bool        empty;
+
+	range_deserialize(typcache, DatumGetRangeTypeP(range), &lower, &upper, &empty);
+
+	// XXX This is just a prototype function, we do not check for emptiness nor infinity yet...
+	// We will use RangeBounds in the future directly...
+	return lower.val;
+}
+
+static Datum
+getUpper(Datum range, TypeCacheEntry *typcache)
+{
+	RangeBound  lower;
+	RangeBound  upper;
+	bool        empty;
+
+	range_deserialize(typcache, DatumGetRangeTypeP(range), &lower, &upper, &empty);
+
+	// XXX This is just a prototype function, we do not check for emptiness nor infinity yet...
+	// We will use RangeBounds in the future directly...
+	return upper.val;
+}
+
+/*
+ * Return 0 if point is inside range, <0 if the point is right-of the second, or
+ * >0 if the point is left-of the range.
+ *
+ * This should ideally be done with RangeBound types on the right-hand-side
+ * created during range_split execution. Otherwise, we loose information about
+ * inclusive/exclusive bounds and infinity.
+ */
+static int
+ApplyNormalizeMatch(Datum ldatum, bool lisnull, Datum rdatum, bool risnull,
+					SortSupport ssup, TypeCacheEntry *typcache)
+{
+	RangeBound  lower;
+	RangeBound  upper;
+	bool        empty;
+	int32       result;
+
+	/* can't handle reverse sort order; should be prevented by optimizer */
+	Assert(!ssup->ssup_reverse);
+	Assert(!lisnull || !risnull);
+
+	if (lisnull)
+		return ssup->ssup_nulls_first ? -1 : 1;
+	if (risnull)
+		return ssup->ssup_nulls_first ? 1 : -1;
+
+	range_deserialize(typcache, DatumGetRangeTypeP(ldatum), &lower, &upper, &empty);
+
+	result = DatumGetInt32(FunctionCall2Coll(&typcache->rng_cmp_proc_finfo,
+											 typcache->rng_collation,
+											 lower.val, rdatum));
+	if (result == 1)
+		return 1;
+
+	result = DatumGetInt32(FunctionCall2Coll(&typcache->rng_cmp_proc_finfo,
+											 typcache->rng_collation,
+											 upper.val, rdatum));
+	if (result == 1)
+		return 0;
+
+	return -1;
+}
+
 /*
  * MJEvalOuterValues
  *
@@ -418,9 +667,19 @@ MJCompare(MergeJoinState *mergestate)
 			continue;
 		}
 
-		result = ApplySortComparator(clause->ldatum, clause->lisnull,
-									 clause->rdatum, clause->risnull,
-									 &clause->ssup);
+		if (clause->isnormalize)
+		{
+			result = ApplyNormalizeMatch(clause->ldatum, clause->lisnull,
+										 clause->rdatum, clause->risnull,
+										 &clause->ssup, clause->range_typcache);
+		}
+		else
+		{
+			result = ApplySortComparator(clause->ldatum, clause->lisnull,
+										 clause->rdatum, clause->risnull,
+										 &clause->ssup);
+		}
+
 
 		if (result != 0)
 			break;
@@ -611,6 +870,7 @@ ExecMergeJoin(PlanState *pstate)
 	ExprContext *econtext;
 	bool		doFillOuter;
 	bool		doFillInner;
+	TupleTableSlot *out = NULL;
 
 	CHECK_FOR_INTERRUPTS();
 
@@ -656,6 +916,13 @@ ExecMergeJoin(PlanState *pstate)
 				outerTupleSlot = ExecProcNode(outerPlan);
 				node->mj_OuterTupleSlot = outerTupleSlot;
 
+				/* XXX normalize (first call) */
+				if (node->mj_isNormalizer)
+				{
+					node->sameleft = true;
+					ExecCopySlot(node->prev, outerTupleSlot);
+				}
+
 				/* Compute join values and check for unmatchability */
 				switch (MJEvalOuterValues(node))
 				{
@@ -704,6 +971,18 @@ ExecMergeJoin(PlanState *pstate)
 				innerTupleSlot = ExecProcNode(innerPlan);
 				node->mj_InnerTupleSlot = innerTupleSlot;
 
+				/*
+				 * 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).
+				 */
+				if (node->mj_isNormalizer)
+				{
+					node->datumFormat = TupleDescAttr(innerTupleSlot->tts_tupleDescriptor, 0);
+					setSweepline(getLower(slotGetAttrNotNull(outerTupleSlot, 1), testmytypcache));
+					TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+				}
+
 				/* Compute join values and check for unmatchability */
 				switch (MJEvalInnerValues(node, innerTupleSlot))
 				{
@@ -789,6 +1068,10 @@ ExecMergeJoin(PlanState *pstate)
 				innerTupleSlot = node->mj_InnerTupleSlot;
 				econtext->ecxt_innertuple = innerTupleSlot;
 
+				TPGdebugSlot(outerTupleSlot);
+				TPGdebugSlot(node->prev);
+				TPGdebug("sameleft = %d", node->sameleft);
+
 				qualResult = (joinqual == NULL ||
 							  ExecQual(joinqual, econtext));
 				MJ_DEBUG_QUAL(joinqual, qualResult);
@@ -819,13 +1102,56 @@ ExecMergeJoin(PlanState *pstate)
 
 					if (qualResult)
 					{
+						TupleTableSlot *out;
+						bool            isNull;
+						Datum           currP1;
+
 						/*
 						 * qualification succeeded.  now form the desired
 						 * projection tuple and return the slot containing it.
 						 */
 						MJ_printf("ExecMergeJoin: returning tuple\n");
 
-						return ExecProject(node->js.ps.ps_ProjInfo);
+						out = ExecProject(node->js.ps.ps_ProjInfo);
+
+						if (!node->mj_isNormalizer)
+							return out;
+
+						if (node->sameleft)
+						{
+							currP1 = slot_getattr(innerTupleSlot, 1, &isNull);
+							TPGdebugDatum(currP1, node->datumFormat->atttypid);
+							if (node->sweepline < currP1)
+							{
+								temporalAdjustmentStoreTuple(node, outerTupleSlot, out, node->sweepline, currP1, testmytypcache);
+								freeSweepline();
+								setSweepline(currP1);
+
+								TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+								TPGdebugSlot(out);
+
+								return out;
+							}
+
+							ExecCopySlot(node->prev, outerTupleSlot);
+							node->mj_JoinState = EXEC_MJ_NEXTINNER;
+						}
+						else /* not node->sameleft */
+						{
+							Datum prevTe = getUpper(heapGetAttrNotNull(node->prev, 1), testmytypcache);
+
+							if (node->sweepline < prevTe)
+								temporalAdjustmentStoreTuple(node, node->prev, out, node->sweepline, prevTe, testmytypcache);
+
+							ExecCopySlot(node->prev, outerTupleSlot);
+							freeSweepline();
+							setSweepline(getLower(slotGetAttrNotNull(outerTupleSlot, 1), testmytypcache));
+							TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+							node->sameleft = true;
+							node->mj_JoinState = EXEC_MJ_NEXTINNER;
+							TPGdebugSlot(out);
+							return out;
+						}
 					}
 					else
 						InstrCountFiltered2(node, 1);
@@ -845,6 +1171,9 @@ ExecMergeJoin(PlanState *pstate)
 			case EXEC_MJ_NEXTINNER:
 				MJ_printf("ExecMergeJoin: EXEC_MJ_NEXTINNER\n");
 
+				if (node->mj_isNormalizer)
+					node->sameleft = true;
+
 				if (doFillInner && !node->mj_MatchedInner)
 				{
 					/*
@@ -947,6 +1276,9 @@ ExecMergeJoin(PlanState *pstate)
 			case EXEC_MJ_NEXTOUTER:
 				MJ_printf("ExecMergeJoin: EXEC_MJ_NEXTOUTER\n");
 
+				if (node->mj_isNormalizer)
+					node->sameleft = false;
+
 				if (doFillOuter && !node->mj_MatchedOuter)
 				{
 					/*
@@ -962,6 +1294,11 @@ ExecMergeJoin(PlanState *pstate)
 						return result;
 				}
 
+				// FIXME PEMOSER Only for normalizer...
+				TupleTableSlot *out = NULL;
+				if (node->mj_isNormalizer && !TupIsNull(innerTupleSlot))
+					out = ExecProject(node->js.ps.ps_ProjInfo);
+
 				/*
 				 * now we get the next outer tuple, if any
 				 */
@@ -994,6 +1331,19 @@ ExecMergeJoin(PlanState *pstate)
 							node->mj_JoinState = EXEC_MJ_ENDOUTER;
 							break;
 						}
+
+						if (node->mj_isNormalizer && !TupIsNull(node->prev) && !TupIsNull(innerTupleSlot))
+						{
+							MJ_printf("finalize normalizer!!!\n");
+							Datum prevTe = getUpper(heapGetAttrNotNull(node->prev, 1), testmytypcache);
+							TPGdebugDatum(prevTe, node->datumFormat->atttypid);
+							TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+							MJ_debugtup(node->prev);
+							temporalAdjustmentStoreTuple(node, node->prev, out, node->sweepline, prevTe, testmytypcache);
+							node->mj_JoinState = EXEC_MJ_ENDOUTER;
+							return out;
+						}
+
 						/* Otherwise we're done. */
 						return NULL;
 				}
@@ -1048,7 +1398,7 @@ ExecMergeJoin(PlanState *pstate)
 				compareResult = MJCompare(node);
 				MJ_DEBUG_COMPARE(compareResult);
 
-				if (compareResult == 0)
+				if (compareResult == 0 || (node->mj_isNormalizer && node->mj_markSet))
 				{
 					/*
 					 * the merge clause matched so now we restore the inner
@@ -1085,7 +1435,10 @@ ExecMergeJoin(PlanState *pstate)
 						/* we need not do MJEvalInnerValues again */
 					}
 
-					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
+					if (node->mj_isNormalizer)
+						node->mj_JoinState = EXEC_MJ_SKIP_TEST;
+					else
+						node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
 				{
@@ -1190,6 +1543,7 @@ ExecMergeJoin(PlanState *pstate)
 					MarkInnerTuple(node->mj_InnerTupleSlot, node);
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
+					node->mj_markSet = true;
 				}
 				else if (compareResult < 0)
 					node->mj_JoinState = EXEC_MJ_SKIPOUTER_ADVANCE;
@@ -1338,6 +1692,9 @@ ExecMergeJoin(PlanState *pstate)
 			case EXEC_MJ_ENDOUTER:
 				MJ_printf("ExecMergeJoin: EXEC_MJ_ENDOUTER\n");
 
+				if (node->mj_isNormalizer)
+					return NULL;
+
 				Assert(doFillInner);
 
 				if (!node->mj_MatchedInner)
@@ -1439,6 +1796,9 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	TupleDesc	outerDesc,
 				innerDesc;
 	const TupleTableSlotOps *innerOps;
+	const TupleTableSlotOps *prevOps;
+	int         numCols = list_length(node->join.plan.targetlist);
+
 
 	/* check for unsupported flags */
 	Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
@@ -1532,12 +1892,16 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 		ExecInitQual(node->join.joinqual, (PlanState *) mergestate);
 	/* mergeclauses are handled below */
 
+	prevOps = ExecGetResultSlotOps(outerPlanState(mergestate), NULL);
+	mergestate->prev = ExecInitExtraTupleSlot(estate, outerDesc, prevOps);
+
 	/*
 	 * detect whether we need only consider the first matching inner tuple
 	 */
 	mergestate->js.single_match = (node->join.inner_unique ||
 								   node->join.jointype == JOIN_SEMI);
 
+	mergestate->mj_isNormalizer = false;
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
@@ -1587,6 +1951,20 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("FULL JOIN is only supported with merge-joinable join conditions")));
 			break;
+		case JOIN_TEMPORAL_NORMALIZE:
+			mergestate->mj_FillOuter = false;
+			mergestate->mj_FillInner = false;
+			mergestate->mj_isNormalizer = true;
+
+			/* Init buffer values for heap_modify_tuple */
+			mergestate->newValues = palloc0(sizeof(Datum) * numCols);
+			mergestate->nullMask = palloc0(sizeof(bool) * numCols);
+			mergestate->tsteMask = palloc0(sizeof(bool) * numCols);
+
+			/* Not right??? -> Always the last in the list, since we add it during planning phase
+			 * XXX PEMOSER We need to find the correct position of "period" and set that here */
+			mergestate->tsteMask[/*numCols - 1*/0] = true;
+			break;
 		default:
 			elog(ERROR, "unrecognized join type: %d",
 				 (int) node->join.jointype);
diff --git a/src/backend/executor/nodeSort.c b/src/backend/executor/nodeSort.c
index 92855278ad..5f95f7050d 100644
--- a/src/backend/executor/nodeSort.c
+++ b/src/backend/executor/nodeSort.c
@@ -86,6 +86,12 @@ ExecSort(PlanState *pstate)
 		outerNode = outerPlanState(node);
 		tupDesc = ExecGetResultType(outerNode);
 
+		// XXX PEMOSER Manually fix sort operation of second attribute (former time, now upper(time))
+		// We must fix that in general... this is just a proof of concept brute-force solution!
+		if (plannode->plan.lefttree->type == T_ProjectSet) {
+			plannode->sortOperators[0] = 97; // 97 means "<" for int4, it was "<" for int4range
+		}
+
 		tuplesortstate = tuplesort_begin_heap(tupDesc,
 											  plannode->numCols,
 											  plannode->sortColIdx,
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 4b9be13f08..76518c886e 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -4596,6 +4596,7 @@ calc_joinrel_size_estimate(PlannerInfo *root,
 	switch (jointype)
 	{
 		case JOIN_INNER:
+		case JOIN_TEMPORAL_NORMALIZE:
 			nrows = outer_rows * inner_rows * fkselec * jselec;
 			/* pselec not used */
 			break;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index d8ff4bf432..5217c25fc2 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -22,6 +22,11 @@
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 #include "optimizer/planmain.h"
+#include "catalog/pg_operator.h"
+#include "optimizer/tlist.h"
+#include "utils/fmgroids.h"
+#include "nodes/makefuncs.h"
+#include "utils/lsyscache.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -195,7 +200,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	 * way of implementing a full outer join, so override enable_mergejoin if
 	 * it's a full join.
 	 */
-	if (enable_mergejoin || jointype == JOIN_FULL)
+	if (enable_mergejoin || jointype == JOIN_FULL || jointype == JOIN_TEMPORAL_NORMALIZE)
 		extra.mergeclause_list = select_mergejoin_clauses(root,
 														  joinrel,
 														  outerrel,
@@ -937,6 +942,80 @@ sort_inner_and_outer(PlannerInfo *root,
 		Assert(inner_path);
 		jointype = JOIN_INNER;
 	}
+	else if (jointype == JOIN_TEMPORAL_NORMALIZE)
+	{
+		/*
+		 * outer_path is just sort; inner_path is append of (B, ts) projection with (B, te) projection
+		 */
+		List *exprs = NIL; // to collect inner relation's targets
+		FuncExpr *f_split;
+		Var *innervar;
+
+		foreach(l, extra->mergeclause_list)
+		{
+			RestrictInfo       *rinfo = (RestrictInfo *) lfirst(l);
+			Expr               *clause  = (Expr *) rinfo->clause;
+
+			if (IsA(clause, OpExpr))
+			{
+				OpExpr *opexpr = (OpExpr *) clause;
+				if (opexpr->opno == OID_RANGE_EQ_OP)
+				{
+					if (IsA(lsecond(opexpr->args), Var)) {
+
+						// lsecond because it is from the second relation (=inner)
+						innervar = lsecond(opexpr->args);
+
+						f_split = makeFuncExpr(F_RANGE_SPLIT, 23, list_make1(innervar), 0, 0, 0);
+						f_split->funcretset = true;
+
+						/*
+						 * OUTER_VAR cannot be used here, because path creation does not know about it,
+						 * it will be introduced in plan creation.
+						 */
+						innervar = makeVar(2, innervar->varattno, f_split->funcresulttype, -1, 0, 0);
+					}
+				}
+				else
+				{
+					// lsecond because it is from the second relation (=inner)
+					exprs = lappend(exprs, lsecond(opexpr->args));
+				}
+			}
+		}
+
+		RestrictInfo       *rinfo = (RestrictInfo *) linitial(extra->mergeclause_list);
+		OpExpr *opexpr = (OpExpr *) rinfo->clause;
+		lsecond(opexpr->args) = f_split;
+		rinfo->right_em->em_expr = f_split;
+		rinfo->mergeopfamilies = get_mergejoin_opfamilies(opexpr->opno);
+
+		PathTarget *target_split = makeNode(PathTarget);
+		target_split->exprs = lappend(exprs, f_split);
+
+		set_pathtarget_cost_width(root, target_split);
+
+		inner_path = (Path *) create_set_projection_path(root, innerrel, inner_path, target_split);
+		innerrel->reltarget->exprs = inner_path->pathtarget->exprs;//list_make1(innervar);//copyObject(inner_path->pathtarget->exprs);
+		joinrel->reltarget->exprs = list_concat(copyObject(outerrel->reltarget->exprs), innerrel->reltarget->exprs);
+		set_pathtarget_cost_width(root, joinrel->reltarget);
+
+		innerrel->cheapest_total_path = inner_path;
+		innerrel->cheapest_startup_path = inner_path;
+		innerrel->cheapest_parameterized_paths = inner_path;
+		innerrel->pathlist = list_make1(inner_path);
+
+		extra->sjinfo->semi_rhs_exprs = list_make1(f_split);
+		extra->sjinfo->semi_operators = NIL;
+		extra->sjinfo->semi_operators = lappend_oid(extra->sjinfo->semi_operators, 96);
+
+		Assert(inner_path);
+
+		innerrel->cheapest_total_path = inner_path;
+		innerrel->cheapest_startup_path = inner_path;
+		innerrel->cheapest_parameterized_paths = inner_path;
+	}
+
 
 	/*
 	 * If the joinrel is parallel-safe, we may be able to consider a partial
@@ -1028,6 +1107,16 @@ sort_inner_and_outer(PlannerInfo *root,
 		merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
 											 outerkeys);
 
+		if (jointype == JOIN_TEMPORAL_NORMALIZE)
+		{
+			inner_path = (Path *) create_sort_path(root, innerrel, inner_path, innerkeys, -1);
+			innerrel->cheapest_total_path = inner_path;
+			innerrel->cheapest_startup_path = inner_path;
+			innerrel->cheapest_parameterized_paths = inner_path;
+			innerrel->pathlist = list_make1(inner_path);
+			Assert(inner_path);
+		}
+
 		/*
 		 * And now we can make the path.
 		 *
@@ -1360,6 +1449,7 @@ match_unsorted_outer(PlannerInfo *root,
 			break;
 		case JOIN_RIGHT:
 		case JOIN_FULL:
+		case JOIN_TEMPORAL_NORMALIZE:
 			nestjoinOK = false;
 			useallclauses = true;
 			break;
@@ -1686,6 +1776,10 @@ hash_inner_and_outer(PlannerInfo *root,
 	List	   *hashclauses;
 	ListCell   *l;
 
+	/* Hashjoin is not allowed for temporal NORMALIZE */
+	if (jointype == JOIN_TEMPORAL_NORMALIZE)
+		return;
+
 	/*
 	 * We need to build only one hashclauses list for any given pair of outer
 	 * and inner relations; all of the hashable clauses will be used as keys.
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index dfbbfdac6d..5b2354cae5 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -904,6 +904,21 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1,
 								 JOIN_ANTI, sjinfo,
 								 restrictlist);
 			break;
+		case JOIN_TEMPORAL_NORMALIZE:
+			if (is_dummy_rel(rel1) || is_dummy_rel(rel2) ||
+				restriction_is_constant_false(restrictlist, joinrel, false))
+			{
+				mark_dummy_rel(joinrel);
+				break;
+			}
+
+			/*
+			 * Temporal normalization does not support re-ordering of rels
+			 */
+			add_paths_to_joinrel(root, joinrel, rel1, rel2,
+								 JOIN_TEMPORAL_NORMALIZE, sjinfo,
+								 restrictlist);
+			break;
 		default:
 			/* other values not expected here */
 			elog(ERROR, "unrecognized join type: %d", (int) sjinfo->jointype);
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index 56d839bb31..4e74c3439e 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -832,7 +832,12 @@ build_join_pathkeys(PlannerInfo *root,
 					JoinType jointype,
 					List *outer_pathkeys)
 {
-	if (jointype == JOIN_FULL || jointype == JOIN_RIGHT)
+	/*
+	 * TEMPORAL NORMALIZE: To improve this, we would need to remove only
+	 * temporal range types from the path key list, not all
+	 */
+	if (jointype == JOIN_FULL || jointype == JOIN_RIGHT
+			|| jointype == JOIN_TEMPORAL_NORMALIZE)
 		return NIL;
 
 	/*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 236f506cfb..4de92f0b53 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -4217,6 +4217,15 @@ create_mergejoin_plan(PlannerInfo *root,
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	/*
+	 * XXX PEMOSER NORMALIZE needs a result node above to properly
+	 * handle target lists, functions and constants
+	 * Maybe we need to refine this like in create_unique_plan:
+	 * "If the top plan node can't do projections..."
+	 */
+	if (best_path->jpath.jointype == JOIN_TEMPORAL_NORMALIZE)
+		join_plan = make_result(tlist, NULL, join_plan);
+
 	return join_plan;
 }
 
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 2afc3f1dfe..16cfd73f71 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -32,6 +32,7 @@
 #include "parser/analyze.h"
 #include "rewrite/rewriteManip.h"
 #include "utils/lsyscache.h"
+#include "catalog/pg_operator.h"
 
 
 /* These parameters are set by GUC */
@@ -888,6 +889,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
 		switch (j->jointype)
 		{
 			case JOIN_INNER:
+			case JOIN_TEMPORAL_NORMALIZE:
 				leftjoinlist = deconstruct_recurse(root, j->larg,
 												   below_outer_join,
 												   &leftids, &left_inners,
@@ -1010,7 +1012,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
 										*inner_join_rels,
 										j->jointype,
 										my_quals);
-			if (j->jointype == JOIN_SEMI)
+			if (j->jointype == JOIN_SEMI || j->jointype == JOIN_TEMPORAL_NORMALIZE)
 				ojscope = NULL;
 			else
 				ojscope = bms_union(sjinfo->min_lefthand,
@@ -1429,7 +1431,8 @@ compute_semijoin_info(SpecialJoinInfo *sjinfo, List *clause)
 	sjinfo->semi_rhs_exprs = NIL;
 
 	/* Nothing more to do if it's not a semijoin */
-	if (sjinfo->jointype != JOIN_SEMI)
+	if (sjinfo->jointype != JOIN_SEMI
+			&& sjinfo->jointype != JOIN_TEMPORAL_NORMALIZE)
 		return;
 
 	/*
@@ -2613,6 +2616,10 @@ check_mergejoinable(RestrictInfo *restrictinfo)
 	opno = ((OpExpr *) clause)->opno;
 	leftarg = linitial(((OpExpr *) clause)->args);
 
+	// XXX PEMOSER Hardcoded NORMALIZE detection... change this. Read the note below...
+	if (opno == OID_RANGE_EQ_OP)
+		restrictinfo->temp_normalizer = true;
+
 	if (op_mergejoinable(opno, exprType(leftarg)) &&
 		!contain_volatile_functions((Node *) clause))
 		restrictinfo->mergeopfamilies = get_mergejoin_opfamilies(opno);
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index aebe162713..bba27c017a 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -780,6 +780,7 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 		switch (j->jointype)
 		{
 			case JOIN_INNER:
+			case JOIN_TEMPORAL_NORMALIZE:
 				j->larg = pull_up_subqueries_recurse(root, j->larg,
 													 lowest_outer_join,
 													 lowest_nulling_outer_join,
diff --git a/src/backend/optimizer/util/restrictinfo.c b/src/backend/optimizer/util/restrictinfo.c
index 03e5f12d0d..090ca72271 100644
--- a/src/backend/optimizer/util/restrictinfo.c
+++ b/src/backend/optimizer/util/restrictinfo.c
@@ -188,6 +188,7 @@ make_restrictinfo_internal(Expr *clause,
 	restrictinfo->outer_selec = -1;
 
 	restrictinfo->mergeopfamilies = NIL;
+	restrictinfo->temp_normalizer = false;
 
 	restrictinfo->left_ec = NULL;
 	restrictinfo->right_ec = NULL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0279013120..6f2e54db59 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -432,6 +432,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	join_outer join_qual
 %type <jtype>	join_type
 
+%type <node>    normalizer_qual
+%type <jexpr>   normalized_table
+%type <list>	temporal_bounds temporal_bounds_list
+
 %type <list>	extract_list overlay_list position_list
 %type <list>	substr_list trim_list
 %type <list>	opt_interval interval_second
@@ -653,7 +657,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
 
@@ -11866,6 +11870,11 @@ table_ref:	relation_expr opt_alias_clause
 					$2->alias = $4;
 					$$ = (Node *) $2;
 				}
+			| '(' normalized_table ')' alias_clause
+				{
+					$2->alias = $4;
+					$$ = (Node *) $2;
+				}
 		;
 
 
@@ -11984,6 +11993,59 @@ opt_alias_clause: alias_clause						{ $$ = $1; }
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+/*
+ * Temporal alignment statements
+ */
+temporal_bounds: WITH '(' temporal_bounds_list ')'				{ $$ = $3; }
+		;
+
+temporal_bounds_list:
+			columnref
+				{
+					$$ = list_make1($1);
+				}
+			| temporal_bounds_list ',' columnref
+				{
+					$$ = lappend($1, $3);
+				}
+		;
+
+normalizer_qual:
+			USING '(' name_list ')'					{ $$ = (Node *) $3; }
+			| USING '(' ')'							{ $$ = (Node *) NIL; }
+		;
+
+normalized_table:
+			table_ref NORMALIZE table_ref normalizer_qual temporal_bounds
+				{
+					JoinExpr *n = makeNode(JoinExpr);
+					n->jointype = JOIN_TEMPORAL_NORMALIZE;
+					n->isNatural = false;
+					n->larg = $1;
+					n->rarg = $3;
+
+					n->usingClause = NIL;
+
+					if ($4 != NULL && IsA($4, List))
+						n->usingClause = (List *) $4; /* USING clause */
+
+					/*
+					 * A list for our valid-time boundaries with two range typed
+					 * values.
+					 */
+					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;
+				}
+		;
+
 /*
  * func_alias_clause can include both an Alias and a coldeflist, so we make it
  * return a 2-element list that gets disassembled by calling production.
@@ -15396,6 +15458,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 c6ce1011e2..714a88bfa0 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -61,7 +61,7 @@ static void extractRemainingColumns(List *common_colnames,
 						List **res_colnames, List **res_colvars);
 static Node *transformJoinUsingClause(ParseState *pstate,
 						 RangeTblEntry *leftRTE, RangeTblEntry *rightRTE,
-						 List *leftVars, List *rightVars);
+						 List *leftVars, List *rightVars, List *normalizeVars);
 static Node *transformJoinOnClause(ParseState *pstate, JoinExpr *j,
 					  List *namespace);
 static RangeTblEntry *getRTEForSpecialRelationTypes(ParseState *pstate,
@@ -299,7 +299,7 @@ extractRemainingColumns(List *common_colnames,
 static Node *
 transformJoinUsingClause(ParseState *pstate,
 						 RangeTblEntry *leftRTE, RangeTblEntry *rightRTE,
-						 List *leftVars, List *rightVars)
+						 List *leftVars, List *rightVars, List *normalizeVars)
 {
 	Node	   *result;
 	List	   *andargs = NIL;
@@ -333,6 +333,17 @@ transformJoinUsingClause(ParseState *pstate,
 		andargs = lappend(andargs, e);
 	}
 
+	/* Temporal NORMALIZE appends an expression to compare temporal bounds */
+	if (normalizeVars)
+	{
+		A_Expr *e;
+		e = makeSimpleA_Expr(AEXPR_OP, "=",
+							 (Node *) copyObject(linitial(normalizeVars)),
+							 (Node *) copyObject(lsecond(normalizeVars)),
+							 -1);
+		andargs = lappend(andargs, e);
+	}
+
 	/* Only need an AND if there's more than one join column */
 	if (list_length(andargs) == 1)
 		result = (Node *) linitial(andargs);
@@ -1193,6 +1204,7 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		int			sv_namespace_length;
 		RangeTblEntry *rte;
 		int			k;
+		int         isNormalize = (j->jointype == JOIN_TEMPORAL_NORMALIZE);
 
 		/*
 		 * Recursively process the left subtree, then the right.  We must do
@@ -1303,7 +1315,8 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		res_colnames = NIL;
 		res_colvars = NIL;
 
-		if (j->usingClause)
+		/* NORMALIZE supports empty using clauses */
+		if (j->usingClause || (isNormalize && j->usingClause == NIL))
 		{
 			/*
 			 * JOIN/USING (or NATURAL JOIN, as transformed above). Transform
@@ -1313,6 +1326,7 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 			List	   *ucols = j->usingClause;
 			List	   *l_usingvars = NIL;
 			List	   *r_usingvars = NIL;
+			List       *normalize_vars = NIL;
 			ListCell   *ucol;
 
 			Assert(j->quals == NULL);	/* shouldn't have ON() too */
@@ -1398,11 +1412,90 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 														 r_colvar));
 			}
 
+			/* Only for temporal NORMALIZE */
+			if (isNormalize)
+			{
+				int         ndx = 0;
+				ListCell   *col;
+				Var        *l_boundvar;
+				Var        *r_boundvar;
+
+				int         l_bound_index = -1;
+				int         r_bound_index = -1;
+				char       *l_bound;
+				char       *r_bound;
+				ListCell *lboundcol = linitial(((ColumnRef *)linitial(j->temporalBounds))->fields);
+				ListCell *rboundcol = linitial(((ColumnRef *)lsecond(j->temporalBounds))->fields);
+
+				l_bound = strVal(lboundcol);
+				r_bound = strVal(rboundcol);
+
+				/* Find the first bound in left input */
+				foreach(col, l_colnames)
+				{
+					char       *l_colname = strVal(lfirst(col));
+
+					if (strcmp(l_colname, l_bound) == 0)
+					{
+						if (l_bound_index >= 0)
+							ereport(ERROR,
+									(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+											 errmsg("temporal bound name \"%s\" appears more than once in left table",
+													l_bound)));
+						l_bound_index = ndx;
+					}
+					ndx++;
+				}
+
+				if (l_bound_index < 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_COLUMN),
+									 errmsg("column \"%s\" specified in normalizer's WITH clause does not exist in left table",
+											l_bound)));
+
+				/* Find the second bound in right input */
+				ndx = 0;
+				foreach(col, r_colnames)
+				{
+					char       *r_colname = strVal(lfirst(col));
+
+					if (strcmp(r_colname, r_bound) == 0)
+					{
+						if (r_bound_index >= 0)
+							ereport(ERROR,
+									(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+											 errmsg("temporal bound name \"%s\" appears more than once in right table",
+													l_bound)));
+						r_bound_index = ndx;
+					}
+					ndx++;
+				}
+
+				if (r_bound_index < 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_COLUMN),
+									 errmsg("column \"%s\" specified in normalizer's WITH clause does not exist in right table",
+											r_bound)));
+
+				l_boundvar = list_nth(l_colvars, l_bound_index);
+				normalize_vars = lappend(normalize_vars, l_boundvar);
+				r_boundvar = list_nth(r_colvars, r_bound_index);
+				normalize_vars = lappend(normalize_vars, r_boundvar);
+
+				res_colnames = lappend(res_colnames, lboundcol);
+				res_colvars = lappend(res_colvars,
+									  buildMergedJoinVar(pstate,
+														 j->jointype,
+														 l_boundvar,
+														 r_boundvar));
+			}
+
 			j->quals = transformJoinUsingClause(pstate,
 												l_rte,
 												r_rte,
 												l_usingvars,
-												r_usingvars);
+												r_usingvars,
+												normalize_vars);
 		}
 		else if (j->quals)
 		{
@@ -1418,13 +1511,21 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		extractRemainingColumns(res_colnames,
 								l_colnames, l_colvars,
 								&l_colnames, &l_colvars);
-		extractRemainingColumns(res_colnames,
-								r_colnames, r_colvars,
-								&r_colnames, &r_colvars);
+
+		//Temporal normalizers expose only outer relation's columns...
+		if (!isNormalize)
+			extractRemainingColumns(res_colnames,
+									r_colnames, r_colvars,
+									&r_colnames, &r_colvars);
+
 		res_colnames = list_concat(res_colnames, l_colnames);
 		res_colvars = list_concat(res_colvars, l_colvars);
-		res_colnames = list_concat(res_colnames, r_colnames);
-		res_colvars = list_concat(res_colvars, r_colvars);
+
+		if (!isNormalize)
+		{
+			res_colnames = list_concat(res_colnames, r_colnames);
+			res_colvars = list_concat(res_colvars, r_colvars);
+		}
 
 		/*
 		 * Check alias (AS clause), if any.
@@ -1567,6 +1668,7 @@ buildMergedJoinVar(ParseState *pstate, JoinType jointype,
 	switch (jointype)
 	{
 		case JOIN_INNER:
+		case JOIN_TEMPORAL_NORMALIZE:
 
 			/*
 			 * We can use either var; prefer non-coerced one if available.
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index c171c7db28..8e4ddfa473 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -40,6 +40,7 @@
 #include "utils/lsyscache.h"
 #include "utils/rangetypes.h"
 #include "utils/timestamp.h"
+#include "funcapi.h"
 
 
 #define RANGE_EMPTY_LITERAL "empty"
@@ -466,6 +467,79 @@ range_upper(PG_FUNCTION_ARGS)
 	PG_RETURN_DATUM(upper.val);
 }
 
+/* split lower/upper bound into two rows of data */
+Datum
+range_split(PG_FUNCTION_ARGS)
+{
+	typedef struct
+	{
+		RangeBound	lower;
+		RangeBound	upper;
+		bool		empty;
+	} RangeSplitFuncContext;
+
+	FuncCallContext *funcctx;
+	MemoryContext oldcontext;
+	RangeSplitFuncContext *fctx;
+
+	/* stuff done only on the first call of the function */
+	if (SRF_IS_FIRSTCALL())
+	{
+		RangeType  		*r1;
+		TypeCacheEntry  *typcache;
+
+		/* create a function context for cross-call persistence */
+		funcctx = SRF_FIRSTCALL_INIT();
+
+		/*
+		 * switch to memory context appropriate for multiple function calls
+		 */
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		r1 = PG_GETARG_RANGE_P(0);
+
+		/* allocate memory for user context */
+		fctx = (RangeSplitFuncContext *) palloc(sizeof(RangeSplitFuncContext));
+
+		/*
+		 * We cannot use range_get_typecache, because it would overwrite
+		 * fcinfo->flinfo->fn_extra
+		 */
+		typcache = lookup_type_cache(RangeTypeGetOid(r1), TYPECACHE_RANGE_INFO);
+		if (typcache->rngelemtype == NULL)
+			elog(ERROR, "type %u is not a range type", RangeTypeGetOid(r1));
+		range_deserialize(typcache, r1, &fctx->lower, &fctx->upper, &fctx->empty);
+
+		funcctx->user_fctx = fctx;
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	/* stuff done on every call of the function */
+	funcctx = SRF_PERCALL_SETUP();
+	fctx = (RangeSplitFuncContext *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr == 0)
+	{
+		/* Return NULL if there's no finite lower bound */
+		if (fctx->empty || fctx->lower.infinite)
+			SRF_RETURN_NEXT_NULL(funcctx);
+
+		SRF_RETURN_NEXT(funcctx, fctx->lower.val);
+	}
+
+	if (funcctx->call_cntr == 1)
+	{
+		/* Return NULL if there's no finite upper bound */
+		if (fctx->empty || fctx->upper.infinite)
+			SRF_RETURN_NEXT_NULL(funcctx);
+
+		SRF_RETURN_NEXT(funcctx, fctx->upper.val);
+	}
+
+	/* done, after extracting lower and upper bounds */
+	SRF_RETURN_DONE(funcctx);
+}
+
 
 /* range -> bool functions */
 
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index e6837869cf..4d957d5ce2 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -1981,6 +1981,7 @@ eqjoinsel(PG_FUNCTION_ARGS)
 		case JOIN_INNER:
 		case JOIN_LEFT:
 		case JOIN_FULL:
+		case JOIN_TEMPORAL_NORMALIZE:
 			selec = selec_inner;
 			break;
 		case JOIN_SEMI:
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 06aec0780b..333ba207d7 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3060,7 +3060,7 @@
   oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' },
 
 # generic range type operators
-{ oid => '3882', descr => 'equal',
+{ oid => '3882', descr => 'equal', oid_symbol => 'OID_RANGE_EQ_OP',
   oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'anyrange',
   oprright => 'anyrange', oprresult => 'bool', oprcom => '=(anyrange,anyrange)',
   oprnegate => '<>(anyrange,anyrange)', oprcode => 'range_eq',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index a4e173b484..e65894c128 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9427,6 +9427,10 @@
 { oid => '3867',
   proname => 'range_union', prorettype => 'anyrange',
   proargtypes => 'anyrange anyrange', prosrc => 'range_union' },
+{ oid => '4001',
+  descr => 'lower and upper bound of range returned as two tuples',
+  proname => 'range_split', prorettype => 'anyelement', proretset => 't',
+  proargtypes => 'anyrange', prosrc => 'range_split' },
 { oid => '4057',
   descr => 'the smallest range which includes both of the given ranges',
   proname => 'range_merge', prorettype => 'anyrange',
diff --git a/src/include/executor/execdebug.h b/src/include/executor/execdebug.h
index c119fdf4fa..1dd3b2d513 100644
--- a/src/include/executor/execdebug.h
+++ b/src/include/executor/execdebug.h
@@ -82,6 +82,7 @@
  *		sort node debugging defines
  * ----------------
  */
+#define EXEC_SORTDEBUG
 #ifdef EXEC_SORTDEBUG
 #define SO_nodeDisplay(l)				nodeDisplay(l)
 #define SO_printf(s)					printf(s)
@@ -96,14 +97,15 @@
  *		merge join debugging defines
  * ----------------
  */
+#define EXEC_MERGEJOINDEBUG
 #ifdef EXEC_MERGEJOINDEBUG
 
 #define MJ_nodeDisplay(l)				nodeDisplay(l)
-#define MJ_printf(s)					printf(s)
-#define MJ1_printf(s, p)				printf(s, p)
-#define MJ2_printf(s, p1, p2)			printf(s, p1, p2)
-#define MJ_debugtup(slot)				debugtup(slot, NULL)
-#define MJ_dump(state)					ExecMergeTupleDump(state)
+#define MJ_printf(s)					printf(s); fflush(stdout)
+#define MJ1_printf(s, p)				printf(s, p); fflush(stdout)
+#define MJ2_printf(s, p1, p2)			printf(s, p1, p2); fflush(stdout)
+#define MJ_debugtup(slot)				debugtup(slot, NULL); fflush(stdout)
+#define MJ_dump(state)					ExecMergeTupleDump(state); fflush(stdout)
 #define MJ_DEBUG_COMPARE(res) \
   MJ1_printf("  MJCompare() returns %d\n", (res))
 #define MJ_DEBUG_QUAL(clause, res) \
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 09f8217c80..52e16684e7 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1831,6 +1831,17 @@ typedef struct MergeJoinState
 	TupleTableSlot *mj_NullInnerTupleSlot;
 	ExprContext *mj_OuterEContext;
 	ExprContext *mj_InnerEContext;
+
+	/* needed by temporal normalization */
+	bool				 mj_markSet;
+	bool				 mj_isNormalizer;
+	bool				 sameleft;
+	bool				*nullMask;		/* See heap_modify_tuple */
+	bool				*tsteMask;		/* See heap_modify_tuple */
+	Datum				*newValues;		/* tuple values that get updated */
+	Datum				 sweepline;
+	Form_pg_attribute	 datumFormat;	/* Datum format of sweepline, P1, P2 */
+	TupleTableSlot 		*prev;
 } MergeJoinState;
 
 /* ----------------
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index f9389257c6..d61f8d9951 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -84,6 +84,7 @@ typedef enum NodeTag
 	T_SetOp,
 	T_LockRows,
 	T_Limit,
+	T_TemporalAdjustment,
 	/* these aren't subclasses of Plan: */
 	T_NestLoopParam,
 	T_PlanRowMark,
@@ -140,6 +141,7 @@ typedef enum NodeTag
 	T_SetOpState,
 	T_LockRowsState,
 	T_LimitState,
+	T_TemporalAdjustmentState,
 
 	/*
 	 * TAGS FOR PRIMITIVE NODES (primnodes.h)
@@ -256,6 +258,7 @@ typedef enum NodeTag
 	T_LockRowsPath,
 	T_ModifyTablePath,
 	T_LimitPath,
+	T_TemporalAdjustmentPath,
 	/* these aren't subclasses of Path: */
 	T_EquivalenceClass,
 	T_EquivalenceMember,
@@ -476,6 +479,7 @@ typedef enum NodeTag
 	T_PartitionRangeDatum,
 	T_PartitionCmd,
 	T_VacuumRelation,
+	T_TemporalClause,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
@@ -715,7 +719,12 @@ 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
+	 */
+	JOIN_TEMPORAL_NORMALIZE
 
 	/*
 	 * We might need additional join types someday.
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index a008ae07da..71254c5856 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1981,6 +1981,7 @@ typedef struct RestrictInfo
 
 	/* valid if clause is hashjoinable, else InvalidOid: */
 	Oid			hashjoinoperator;	/* copy of clause operator */
+	bool        temp_normalizer;
 
 	/* cache space for hashclause processing; -1 if not yet set */
 	Selectivity left_bucketsize;	/* avg bucketsize of left side */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index a7efae7038..0e2a668d18 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -508,6 +508,7 @@ typedef struct OpExpr
 	Oid			inputcollid;	/* OID of collation that operator should use */
 	List	   *args;			/* arguments to the operator (1 or 2) */
 	int			location;		/* token location, or -1 if unknown */
+	bool		isnormalize;
 } OpExpr;
 
 /*
@@ -1480,6 +1481,8 @@ 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 */
 } JoinExpr;
 
 /*----------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f05444008c..ed6f944593 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -261,6 +261,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)
#60David Steele
david@pgmasters.net
In reply to: Peter Moser (#59)
Re: Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

Hi Peter,

On 2/28/19 11:43 PM, Peter Moser wrote:

Dear all,
we rebased our temporal normalization patch on top of
554ebf687852d045f0418d3242b978b49f160f44 from 2019-02-28.

I will also add this prototype (WIP) patch to the commitfest of March,
as suggested by two developers met at the
FOSDEM some weeks ago.

I have marked this entry as targeting PG13 since it is too late to
consider for PG12.

Regards,
--
-David
david@pgmasters.net

#61Robert Haas
robertmhaas@gmail.com
In reply to: David Steele (#60)
Re: Re: [HACKERS] [PROPOSAL] Temporal query processing with range types

On Tue, Mar 5, 2019 at 3:46 AM David Steele <david@pgmasters.net> wrote:

I have marked this entry as targeting PG13 since it is too late to
consider for PG12.

Sounds right. As Peter said himself, this patch is WIP, so it's too
soon to consider integrating it. This is also fairly evident from the
content of the patch, which is full of comments marked XXX and PEMOSER
that obviously need to be addressed somehow. For all of that, I'd say
this patch is much closer to being on the right track than the old
design, even though it's clear that a lot of work remains.

Some desultory review comments:

+#define setSweepline(datum) \
+ node->sweepline = datumCopy(datum, node->datumFormat->attbyval,
node->datumFormat->attlen)
+
+#define freeSweepline() \
+ if (! node->datumFormat->attbyval) pfree(DatumGetPointer(node->sweepline))

I am quite dubious about this. Almost everywhere in the executor we
rely on slots to keep track of tuples and free memory for us. It seems
unlikely that this should be the one place where we have code that
looks completely different. Aside from that, this seems to propose
there is only one relevant column, which seems like an assumption that
we probably don't want to bake too deeply into the code.

+ ereport(ERROR,
+ (errcode(ERRCODE_NOT_NULL_VIOLATION),
+ errmsg("Attribute \"%s\" at position %d is null. Temporal " \
+ "adjustment not possible.",
+ NameStr(TupleDescAttr(slot->tts_tupleDescriptor, attnum - 1)->attname),
+ attnum)));

This error message is marked as translatable (ereport used rather than
elog) but the error-message text is unsuitable for a user-facing
error. If this is a user-facing error, we need a better error, or
maybe we need to rethink the semantics altogether (e.g. just skip such
rows instead of erroring out, or something). If it's an internal
error that should not be possible no matter what query the user
enters, and is only here as a sanity test, just simplify and use elog
(and maybe add some comments explaining why that's so).

+ * heapGetAttrNotNull

I may be a bit behind the times here, but it seems to me that this is
functionally equivalent to slotGetAttrNotNull and thus we shouldn't
need both.

+ bool empty = false;

Not much point in declaring a variable whose value is never changed
and whose value takes up exactly the same number of characters as the
variable name.

+ * 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.

This doesn't seem to describe what the function is actually doing.

+ * This should ideally be done with RangeBound types on the right-hand-side
+ * created during range_split execution. Otherwise, we loose information about
+ * inclusive/exclusive bounds and infinity. We would need to implement btree
+ * operators for RangeBounds.

This seems like an idea for future improvement, but it's unclear to me
how the proposed idea is different from the state created by the
patch.

Also, materializing the slot to a heap tuple so that we can modify it
seems inefficient. I wonder if we can't use a virtual slot here.

+ if (qual->opno == OID_RANGE_EQ_OP) {
+ Oid rngtypid;
+
+ // XXX PEMOSER Change opfamily and opfunc
+ qual->opfuncid = F_RANGE_CONTAINS; //<<--- opfuncid can be 0 during planning
+ qual->opno = OID_RANGE_CONTAINS_ELEM_OP; //OID_RANGE_CONTAINS_OP;
+ clause->isnormalize = true;
+
+ // Attention: cannot merge using non-equality operator 3890 <---
OID_RANGE_CONTAINS_OP
+ opfamily = 4103; //range_inclusion_ops from pg_opfamily.h
+
+ rngtypid = exprType((Node*)clause->lexpr->expr);
+ clause->range_typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
+ testmytypcache = clause->range_typcache;
+ } else {
+ clause->isnormalize = false;
+ }

This is pretty obviously a mess of hardcoded constants which are,
furthermore, not explained. I can't tell whether this is intended as
a dirty hack to make some cases work while other things remain broken,
or whether you believe that OID_RANGE_EQ_OP. If it's the latter, this
needs a much better explanation. You probably realize that
"Attention: cannot merge using non-equality operator 3890" is not a
compelling justification, and that hard-coding all of these things
here doesn't look good.

In general, this patch needs both user-facing documentation and more
and better code comments. I would suggest writing the user-facing
documentation soon. It is pretty clear that you've got the basics of
this working, but it's impossible to understand what the semantics are
supposed to be except by staring at the code until you figure it out,
or running experiments. People who are interested in this
functionality are more likely to provide useful feedback if there's
something they can read and go "yeah, that looks right" or "wait, that
sounds wrong." Also, there may be places where, in the process of
documenting, you realize that things should be changed to make them
more consistent or easier to understand. Adding a developer README to
the patch might be good, too.

With respect to this specific point, I'm wondering if the patch is
trying to handle two cases in somewhat different ways -- one where
there are actual range types involved and the other where there are
two different columns that can be view as constituting a range. That
might explain the special-casing here, but if so there is probably a
need to clearly distinguish those cases much earlier, probably
someplace in the planner, not just at execution time.

+static Datum
+getLower(Datum range, TypeCacheEntry *typcache)
...
+static Datum
+getUpper(Datum range, TypeCacheEntry *typcache)

Anything that calls both of these functions is going to call
range_deserialize() twice, which seems inefficient.

+ if (node->sweepline < currP1)

This looks very strange. Those are just Datums. Typically what you'd
do is determine based on the datatype which opclass's comparator to
use and then call that. I'm not sure what purpose could be served by
comparing Datums directly. Maybe it would give you sensible answers
if these are integers, but not if they're pointers and probably not
even if they are native floats.

+ // XXX PEMOSER Manually fix sort operation of second attribute
(former time, now upper(time))
+ // We must fix that in general... this is just a proof of concept
brute-force solution!
+ if (plannode->plan.lefttree->type == T_ProjectSet) {
+ plannode->sortOperators[0] = 97; // 97 means "<" for int4, it was
"<" for int4range
+ }

Aside from the fact that this shouldn't be hard-coded, this shouldn't
be in the executor at all. The planner should be figuring this out
for us. If the planner is making a bad decision, then it needs to be
fixed so that it makes a good decision. Just trying to work around
the wrong decision elsewhere in the code won't lead to anything good.

+ /*
+ * TEMPORAL NORMALIZE: To improve this, we would need to remove only
+ * temporal range types from the path key list, not all
+ */

If you can do a temporal join that has leading columns being joined in
a standard way, then it might be possible to truncate the PathKey list
to include just the leading columns, provided the join preserves
ordering on those columns -- but once you get to the first column in
the PathKey where ordering is not preserved, any columns after that
point are certainly no longer part of the PathKey.

+ /*
+ * XXX PEMOSER NORMALIZE needs a result node above to properly
+ * handle target lists, functions and constants
+ * Maybe we need to refine this like in create_unique_plan:
+ * "If the top plan node can't do projections..."
+ */
+ if (best_path->jpath.jointype == JOIN_TEMPORAL_NORMALIZE)
+ join_plan = make_result(tlist, NULL, join_plan);

Actually, I think what you need to do is make sure that MergeJoin can
still project in all cases after your changes. Breaking that only for
the temporal normalize case isn't going to be acceptable, and it
shouldn't be hard to avoid having such a restriction. It's "just" a
matter of figuring out how a bunch of fiddly executor bits work...

+ // XXX PEMOSER Hardcoded NORMALIZE detection... change this. Read
the note below...

What note below?

+ /* Temporal NORMALIZE appends an expression to compare temporal bounds */
+ if (normalizeVars)
+ {
+ A_Expr *e;
+ e = makeSimpleA_Expr(AEXPR_OP, "=",
+ (Node *) copyObject(linitial(normalizeVars)),
+ (Node *) copyObject(lsecond(normalizeVars)),
+ -1);
+ andargs = lappend(andargs, e);
+ }

You absolutely cannot do stuff like this in parse analysis. It's
doubtful whether it's acceptable in general to include hard-coded
knowledge of = as a general point, but it's certainly not OK to do it
in parse analysis. Parse analysis's job is to determine the objects
to which the query refers, NOT to massage it as a preparation for
further optimization.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#62Ibrar Ahmed
ibrar.ahmad@gmail.com
In reply to: Robert Haas (#61)
Re: [PROPOSAL] Temporal query processing with range types

I start looking at the patch, there is a couple of problems with the patch. The first one is the OID conflict, which I fixed on my machine. The second problem is assertion failure. I think you have not compiled the PostgreSQL code with the assertion.

...
postgres=# SELECT *
FROM (projects p1 NORMALIZE projects p2 USING() WITH(t,t)) p_adjusted;
TRAP: FailedAssertion("!(ptr == ((void *)0) || (((const Node*)(ptr))->type) == type)", File: "../../../src/include/nodes/nodes.h", Line: 588)
psql: server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
The connection to the server was lost. Attempting reset: 2019-04-02 12:50:09.654 UTC [27550] LOG: server process (PID 27559) was terminated by signal 6: Aborted
...

Although this patch is WIP, but please avoid mix declaration to avoid the compiler warning message.

...
joinpath.c:993:3: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
PathTarget *target_split = makeNode(PathTarget);
...

I am still looking at the patch.

#63Thomas Munro
thomas.munro@gmail.com
In reply to: Ibrar Ahmed (#62)
Re: [PROPOSAL] Temporal query processing with range types

On Wed, Apr 3, 2019 at 2:12 AM Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:

I start looking at the patch, there is a couple of problems with the patch. The first one is the OID conflict, which I fixed on my machine. The second problem is assertion failure. I think you have not compiled the PostgreSQL code with the assertion.

Hi Peter,

Looks like there was some good feedback for this WIP project last time
around. It's currently in "Needs Review" status in the July
Commitfest. To encourage more review and see some automated compile
and test results, could we please have a fresh rebase? The earlier
patches no longer apply.

Thanks,

--
Thomas Munro
https://enterprisedb.com

#64Ibrar Ahmed
ibrar.ahmad@gmail.com
In reply to: Thomas Munro (#63)
1 attachment(s)
Re: [PROPOSAL] Temporal query processing with range types

Hi,
I have rebased the patch and currently reviewing the patch
on master (1e2fddfa33d3c7cc93ca3ee0f32852699bd3e012).

On Mon, Jul 1, 2019 at 4:45 PM Thomas Munro <thomas.munro@gmail.com> wrote:

On Wed, Apr 3, 2019 at 2:12 AM Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:

I start looking at the patch, there is a couple of problems with the

patch. The first one is the OID conflict, which I fixed on my machine. The
second problem is assertion failure. I think you have not compiled the
PostgreSQL code with the assertion.

Hi Peter,

Looks like there was some good feedback for this WIP project last time
around. It's currently in "Needs Review" status in the July
Commitfest. To encourage more review and see some automated compile
and test results, could we please have a fresh rebase? The earlier
patches no longer apply.

Thanks,

--
Thomas Munro
https://enterprisedb.com

--
Ibrar Ahmed

Attachments:

001_temporal_query_processing_with_range_types_v4.patchapplication/octet-stream; name=001_temporal_query_processing_with_range_types_v4.patchDownload
diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c
index 2a1d000b03..a309596fa1 100644
--- a/src/backend/executor/nodeMergejoin.c
+++ b/src/backend/executor/nodeMergejoin.c
@@ -99,6 +99,106 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
+// XXX TEMPORAL NORMALIZE PEMOSER ----------------------------
+// !!! THis is just for prototyping, delete asap...
+
+#include "catalog/pg_operator.h"
+#include "nodes/nodeFuncs.h"
+#include "utils/fmgroids.h"
+#include "utils/rangetypes.h"
+#include "utils/typcache.h"
+#include "access/htup_details.h"                /* for heap_getattr */
+#include "nodes/print.h"                        /* for print_slot */
+#include "utils/datum.h"                        /* for datumCopy */
+
+
+
+#define TEMPORAL_DEBUG
+/*
+ * #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
+
+TypeCacheEntry *testmytypcache;
+#define setSweepline(datum) \
+	node->sweepline = datumCopy(datum, node->datumFormat->attbyval, node->datumFormat->attlen)
+
+#define freeSweepline() \
+	if (! node->datumFormat->attbyval) pfree(DatumGetPointer(node->sweepline))
+
+ /*
+  * 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(TupleDescAttr(slot->tts_tupleDescriptor, 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;
+	HeapTuple tuple;
+
+	tuple = ExecFetchSlotHeapTuple(slot, true, NULL);
+	result = heap_getattr(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(TupleDescAttr(slot->tts_tupleDescriptor,
+								attnum - 1)->attname),
+						attnum)));
+
+	return result;
+}
+
+// XXX TEMPORAL NORMALIZE PEMOSER END ------------------------
+
 
 /*
  * States of the ExecMergeJoin state machine
@@ -138,6 +238,10 @@ typedef struct MergeJoinClauseData
 	 * stored here.
 	 */
 	SortSupportData ssup;
+
+	/* needed for Temporal Normalization */
+	bool             isnormalize;
+	TypeCacheEntry  *range_typcache;
 }			MergeJoinClauseData;
 
 /* Result type for MJEvalOuterValues and MJEvalInnerValues */
@@ -152,6 +256,59 @@ typedef enum
 #define MarkInnerTuple(innerTupleSlot, mergestate) \
 	ExecCopySlot((mergestate)->mj_MarkedTupleSlot, (innerTupleSlot))
 
+/*
+ * 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(MergeJoinState *mergestate,
+							 TupleTableSlot* slotToModify,
+							 TupleTableSlot* slotToStoreIn,
+							 Datum ts,
+							 Datum te,
+							 TypeCacheEntry *typcache)
+{
+	MemoryContext	oldContext;
+	HeapTuple		t;
+	RangeBound  	lower;
+	RangeBound  	upper;
+	bool        	empty = false;
+	HeapTuple       tuple;
+
+	/*
+	 * This should ideally be done with RangeBound types on the right-hand-side
+	 * created during range_split execution. Otherwise, we loose information about
+	 * inclusive/exclusive bounds and infinity. We would need to implement btree
+	 * operators for RangeBounds.
+	 */
+	lower.val = ts;
+	lower.lower = true;
+	lower.infinite = false;
+	lower.inclusive = true;
+
+	upper.val = te;
+	upper.lower = false;
+	upper.infinite = false;
+	upper.inclusive = false;
+
+	mergestate->newValues[0] = (Datum) make_range(typcache, &lower, &upper, empty);
+
+	oldContext = MemoryContextSwitchTo(mergestate->js.ps.ps_ResultTupleSlot->tts_mcxt);
+	tuple = ExecFetchSlotHeapTuple(slotToModify, true, NULL);
+	t = heap_modify_tuple(tuple,
+						  slotToModify->tts_tupleDescriptor,
+						  mergestate->newValues,
+						  mergestate->nullMask,
+						  mergestate->tsteMask);
+	MemoryContextSwitchTo(oldContext);
+	ExecForceStoreHeapTuple(t, slotToStoreIn);
+
+	TPGdebug("Storing tuple:");
+	TPGdebugSlot(slotToStoreIn);
+}
+
 
 /*
  * MJExamineQuals
@@ -201,6 +358,8 @@ MJExamineQuals(List *mergeclauses,
 		Oid			op_righttype;
 		Oid			sortfunc;
 
+		pprint(qual);
+
 		if (!IsA(qual, OpExpr))
 			elog(ERROR, "mergejoin clause is not an OpExpr");
 
@@ -221,12 +380,31 @@ MJExamineQuals(List *mergeclauses,
 			elog(ERROR, "unsupported mergejoin strategy %d", opstrategy);
 		clause->ssup.ssup_nulls_first = nulls_first;
 
+		if (qual->opno == OID_RANGE_EQ_OP) {
+			Oid rngtypid;
+
+			// XXX PEMOSER Change opfamily and opfunc
+			qual->opfuncid = F_RANGE_CONTAINS; //<<--- opfuncid can be 0 during planning
+			qual->opno = OID_RANGE_CONTAINS_ELEM_OP; //OID_RANGE_CONTAINS_OP;
+			clause->isnormalize = true;
+
+			// Attention: cannot merge using non-equality operator 3890 <--- OID_RANGE_CONTAINS_OP
+			opfamily = 4103; //range_inclusion_ops from pg_opfamily.h
+
+			rngtypid = exprType((Node*)clause->lexpr->expr);
+			clause->range_typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
+			testmytypcache = clause->range_typcache;
+		} else {
+			clause->isnormalize = false;
+		}
+
+
 		/* Extract the operator's declared left/right datatypes */
 		get_op_opfamily_properties(qual->opno, opfamily, false,
 								   &op_strategy,
 								   &op_lefttype,
 								   &op_righttype);
-		if (op_strategy != BTEqualStrategyNumber)	/* should not happen */
+		if (op_strategy != BTEqualStrategyNumber && !clause->isnormalize)   /* should not happen */
 			elog(ERROR, "cannot merge using non-equality operator %u",
 				 qual->opno);
 
@@ -249,7 +427,7 @@ MJExamineQuals(List *mergeclauses,
 			/* The sort support function can provide a comparator */
 			OidFunctionCall1(sortfunc, PointerGetDatum(&clause->ssup));
 		}
-		if (clause->ssup.comparator == NULL)
+		if (clause->ssup.comparator == NULL && !clause->isnormalize)
 		{
 			/* support not available, get comparison func */
 			sortfunc = get_opfamily_proc(opfamily,
@@ -269,6 +447,77 @@ MJExamineQuals(List *mergeclauses,
 	return clauses;
 }
 
+static Datum
+getLower(Datum range, TypeCacheEntry *typcache)
+{
+	RangeBound  lower;
+	RangeBound  upper;
+	bool        empty;
+
+	range_deserialize(typcache, DatumGetRangeTypeP(range), &lower, &upper, &empty);
+
+	// XXX This is just a prototype function, we do not check for emptiness nor infinity yet...
+	// We will use RangeBounds in the future directly...
+	return lower.val;
+}
+
+static Datum
+getUpper(Datum range, TypeCacheEntry *typcache)
+{
+	RangeBound  lower;
+	RangeBound  upper;
+	bool        empty;
+
+	range_deserialize(typcache, DatumGetRangeTypeP(range), &lower, &upper, &empty);
+
+	// XXX This is just a prototype function, we do not check for emptiness nor infinity yet...
+	// We will use RangeBounds in the future directly...
+	return upper.val;
+}
+
+/*
+ * Return 0 if point is inside range, <0 if the point is right-of the second, or
+ * >0 if the point is left-of the range.
+ *
+ * This should ideally be done with RangeBound types on the right-hand-side
+ * created during range_split execution. Otherwise, we loose information about
+ * inclusive/exclusive bounds and infinity.
+ */
+static int
+ApplyNormalizeMatch(Datum ldatum, bool lisnull, Datum rdatum, bool risnull,
+					SortSupport ssup, TypeCacheEntry *typcache)
+{
+	RangeBound  lower;
+	RangeBound  upper;
+	bool        empty;
+	int32       result;
+
+	/* can't handle reverse sort order; should be prevented by optimizer */
+	Assert(!ssup->ssup_reverse);
+	Assert(!lisnull || !risnull);
+
+	if (lisnull)
+		return ssup->ssup_nulls_first ? -1 : 1;
+	if (risnull)
+		return ssup->ssup_nulls_first ? 1 : -1;
+
+	range_deserialize(typcache, DatumGetRangeTypeP(ldatum), &lower, &upper, &empty);
+
+	result = DatumGetInt32(FunctionCall2Coll(&typcache->rng_cmp_proc_finfo,
+											 typcache->rng_collation,
+											 lower.val, rdatum));
+	if (result == 1)
+		return 1;
+
+	result = DatumGetInt32(FunctionCall2Coll(&typcache->rng_cmp_proc_finfo,
+											 typcache->rng_collation,
+											 upper.val, rdatum));
+	if (result == 1)
+		return 0;
+
+	return -1;
+}
+
 /*
  * MJEvalOuterValues
  *
@@ -418,9 +667,19 @@ MJCompare(MergeJoinState *mergestate)
 			continue;
 		}
 
-		result = ApplySortComparator(clause->ldatum, clause->lisnull,
-									 clause->rdatum, clause->risnull,
-									 &clause->ssup);
+		if (clause->isnormalize)
+		{
+			result = ApplyNormalizeMatch(clause->ldatum, clause->lisnull,
+										 clause->rdatum, clause->risnull,
+										 &clause->ssup, clause->range_typcache);
+		}
+		else
+		{
+			result = ApplySortComparator(clause->ldatum, clause->lisnull,
+										 clause->rdatum, clause->risnull,
+										 &clause->ssup);
+		}
+
 
 		if (result != 0)
 			break;
@@ -611,6 +870,7 @@ ExecMergeJoin(PlanState *pstate)
 	ExprContext *econtext;
 	bool		doFillOuter;
 	bool		doFillInner;
+	TupleTableSlot *out = NULL;
 
 	CHECK_FOR_INTERRUPTS();
 
@@ -656,6 +916,13 @@ ExecMergeJoin(PlanState *pstate)
 				outerTupleSlot = ExecProcNode(outerPlan);
 				node->mj_OuterTupleSlot = outerTupleSlot;
 
+				/* XXX normalize (first call) */
+				if (node->mj_isNormalizer)
+				{
+					node->sameleft = true;
+					ExecCopySlot(node->prev, outerTupleSlot);
+				}
+
 				/* Compute join values and check for unmatchability */
 				switch (MJEvalOuterValues(node))
 				{
@@ -704,6 +971,18 @@ ExecMergeJoin(PlanState *pstate)
 				innerTupleSlot = ExecProcNode(innerPlan);
 				node->mj_InnerTupleSlot = innerTupleSlot;
 
+				/*
+				 * 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).
+				 */
+				if (node->mj_isNormalizer)
+				{
+					node->datumFormat = TupleDescAttr(innerTupleSlot->tts_tupleDescriptor, 0);
+					setSweepline(getLower(slotGetAttrNotNull(outerTupleSlot, 1), testmytypcache));
+					TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+				}
+
 				/* Compute join values and check for unmatchability */
 				switch (MJEvalInnerValues(node, innerTupleSlot))
 				{
@@ -789,6 +1068,10 @@ ExecMergeJoin(PlanState *pstate)
 				innerTupleSlot = node->mj_InnerTupleSlot;
 				econtext->ecxt_innertuple = innerTupleSlot;
 
+				TPGdebugSlot(outerTupleSlot);
+				TPGdebugSlot(node->prev);
+				TPGdebug("sameleft = %d", node->sameleft);
+
 				qualResult = (joinqual == NULL ||
 							  ExecQual(joinqual, econtext));
 				MJ_DEBUG_QUAL(joinqual, qualResult);
@@ -819,13 +1102,56 @@ ExecMergeJoin(PlanState *pstate)
 
 					if (qualResult)
 					{
+						TupleTableSlot *out;
+						bool            isNull;
+						Datum           currP1;
+
 						/*
 						 * qualification succeeded.  now form the desired
 						 * projection tuple and return the slot containing it.
 						 */
 						MJ_printf("ExecMergeJoin: returning tuple\n");
 
-						return ExecProject(node->js.ps.ps_ProjInfo);
+						out = ExecProject(node->js.ps.ps_ProjInfo);
+
+						if (!node->mj_isNormalizer)
+							return out;
+
+						if (node->sameleft)
+						{
+							currP1 = slot_getattr(innerTupleSlot, 1, &isNull);
+							TPGdebugDatum(currP1, node->datumFormat->atttypid);
+							if (node->sweepline < currP1)
+							{
+								temporalAdjustmentStoreTuple(node, outerTupleSlot, out, node->sweepline, currP1, testmytypcache);
+								freeSweepline();
+								setSweepline(currP1);
+
+								TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+								TPGdebugSlot(out);
+
+								return out;
+							}
+
+							ExecCopySlot(node->prev, outerTupleSlot);
+							node->mj_JoinState = EXEC_MJ_NEXTINNER;
+						}
+						else /* not node->sameleft */
+						{
+							Datum prevTe = getUpper(heapGetAttrNotNull(node->prev, 1), testmytypcache);
+
+							if (node->sweepline < prevTe)
+								temporalAdjustmentStoreTuple(node, node->prev, out, node->sweepline, prevTe, testmytypcache);
+
+							ExecCopySlot(node->prev, outerTupleSlot);
+							freeSweepline();
+							setSweepline(getLower(slotGetAttrNotNull(outerTupleSlot, 1), testmytypcache));
+							TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+							node->sameleft = true;
+							node->mj_JoinState = EXEC_MJ_NEXTINNER;
+							TPGdebugSlot(out);
+							return out;
+						}
 					}
 					else
 						InstrCountFiltered2(node, 1);
@@ -845,6 +1171,9 @@ ExecMergeJoin(PlanState *pstate)
 			case EXEC_MJ_NEXTINNER:
 				MJ_printf("ExecMergeJoin: EXEC_MJ_NEXTINNER\n");
 
+				if (node->mj_isNormalizer)
+					node->sameleft = true;
+
 				if (doFillInner && !node->mj_MatchedInner)
 				{
 					/*
@@ -947,6 +1276,9 @@ ExecMergeJoin(PlanState *pstate)
 			case EXEC_MJ_NEXTOUTER:
 				MJ_printf("ExecMergeJoin: EXEC_MJ_NEXTOUTER\n");
 
+				if (node->mj_isNormalizer)
+					node->sameleft = false;
+
 				if (doFillOuter && !node->mj_MatchedOuter)
 				{
 					/*
@@ -962,6 +1294,11 @@ ExecMergeJoin(PlanState *pstate)
 						return result;
 				}
 
+				// FIXME PEMOSER Only for normalizer...
+				TupleTableSlot *out = NULL;
+				if (node->mj_isNormalizer && !TupIsNull(innerTupleSlot))
+					out = ExecProject(node->js.ps.ps_ProjInfo);
+
 				/*
 				 * now we get the next outer tuple, if any
 				 */
@@ -994,6 +1331,19 @@ ExecMergeJoin(PlanState *pstate)
 							node->mj_JoinState = EXEC_MJ_ENDOUTER;
 							break;
 						}
+
+						if (node->mj_isNormalizer && !TupIsNull(node->prev) && !TupIsNull(innerTupleSlot))
+						{
+							MJ_printf("finalize normalizer!!!\n");
+							Datum prevTe = getUpper(heapGetAttrNotNull(node->prev, 1), testmytypcache);
+							TPGdebugDatum(prevTe, node->datumFormat->atttypid);
+							TPGdebugDatum(node->sweepline, node->datumFormat->atttypid);
+							MJ_debugtup(node->prev);
+							temporalAdjustmentStoreTuple(node, node->prev, out, node->sweepline, prevTe, testmytypcache);
+							node->mj_JoinState = EXEC_MJ_ENDOUTER;
+							return out;
+						}
+
 						/* Otherwise we're done. */
 						return NULL;
 				}
@@ -1048,7 +1398,7 @@ ExecMergeJoin(PlanState *pstate)
 				compareResult = MJCompare(node);
 				MJ_DEBUG_COMPARE(compareResult);
 
-				if (compareResult == 0)
+				if (compareResult == 0 || (node->mj_isNormalizer && node->mj_markSet))
 				{
 					/*
 					 * the merge clause matched so now we restore the inner
@@ -1085,7 +1435,10 @@ ExecMergeJoin(PlanState *pstate)
 						/* we need not do MJEvalInnerValues again */
 					}
 
-					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
+					if (node->mj_isNormalizer)
+						node->mj_JoinState = EXEC_MJ_SKIP_TEST;
+					else
+						node->mj_JoinState = EXEC_MJ_JOINTUPLES;
 				}
 				else
 				{
@@ -1190,6 +1543,7 @@ ExecMergeJoin(PlanState *pstate)
 					MarkInnerTuple(node->mj_InnerTupleSlot, node);
 
 					node->mj_JoinState = EXEC_MJ_JOINTUPLES;
+					node->mj_markSet = true;
 				}
 				else if (compareResult < 0)
 					node->mj_JoinState = EXEC_MJ_SKIPOUTER_ADVANCE;
@@ -1338,6 +1692,9 @@ ExecMergeJoin(PlanState *pstate)
 			case EXEC_MJ_ENDOUTER:
 				MJ_printf("ExecMergeJoin: EXEC_MJ_ENDOUTER\n");
 
+				if (node->mj_isNormalizer)
+					return NULL;
+
 				Assert(doFillInner);
 
 				if (!node->mj_MatchedInner)
@@ -1439,6 +1796,9 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 	TupleDesc	outerDesc,
 				innerDesc;
 	const TupleTableSlotOps *innerOps;
+	const TupleTableSlotOps *prevOps;
+	int         numCols = list_length(node->join.plan.targetlist);
+
 
 	/* check for unsupported flags */
 	Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
@@ -1532,12 +1892,16 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 		ExecInitQual(node->join.joinqual, (PlanState *) mergestate);
 	/* mergeclauses are handled below */
 
+	prevOps = ExecGetResultSlotOps(outerPlanState(mergestate), NULL);
+	mergestate->prev = ExecInitExtraTupleSlot(estate, outerDesc, prevOps);
+
 	/*
 	 * detect whether we need only consider the first matching inner tuple
 	 */
 	mergestate->js.single_match = (node->join.inner_unique ||
 								   node->join.jointype == JOIN_SEMI);
 
+	mergestate->mj_isNormalizer = false;
 	/* set up null tuples for outer joins, if needed */
 	switch (node->join.jointype)
 	{
@@ -1587,6 +1951,20 @@ ExecInitMergeJoin(MergeJoin *node, EState *estate, int eflags)
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("FULL JOIN is only supported with merge-joinable join conditions")));
 			break;
+		case JOIN_TEMPORAL_NORMALIZE:
+			mergestate->mj_FillOuter = false;
+			mergestate->mj_FillInner = false;
+			mergestate->mj_isNormalizer = true;
+
+			/* Init buffer values for heap_modify_tuple */
+			mergestate->newValues = palloc0(sizeof(Datum) * numCols);
+			mergestate->nullMask = palloc0(sizeof(bool) * numCols);
+			mergestate->tsteMask = palloc0(sizeof(bool) * numCols);
+
+			/* Not right??? -> Always the last in the list, since we add it during planning phase
+			 * XXX PEMOSER We need to find the correct position of "period" and set that here */
+			mergestate->tsteMask[/*numCols - 1*/0] = true;
+			break;
 		default:
 			elog(ERROR, "unrecognized join type: %d",
 				 (int) node->join.jointype);
diff --git a/src/backend/executor/nodeSort.c b/src/backend/executor/nodeSort.c
index 92855278ad..5f95f7050d 100644
--- a/src/backend/executor/nodeSort.c
+++ b/src/backend/executor/nodeSort.c
@@ -86,6 +86,12 @@ ExecSort(PlanState *pstate)
 		outerNode = outerPlanState(node);
 		tupDesc = ExecGetResultType(outerNode);
 
+		// XXX PEMOSER Manually fix sort operation of second attribute (former time, now upper(time))
+		// We must fix that in general... this is just a proof of concept brute-force solution!
+		if (plannode->plan.lefttree->type == T_ProjectSet) {
+			plannode->sortOperators[0] = 97; // 97 means "<" for int4, it was "<" for int4range
+		}
+
 		tuplesortstate = tuplesort_begin_heap(tupDesc,
 											  plannode->numCols,
 											  plannode->sortColIdx,
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 3a9a994733..6ccb84d314 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -4655,6 +4655,7 @@ calc_joinrel_size_estimate(PlannerInfo *root,
 	switch (jointype)
 	{
 		case JOIN_INNER:
+		case JOIN_TEMPORAL_NORMALIZE:
 			nrows = outer_rows * inner_rows * fkselec * jselec;
 			/* pselec not used */
 			break;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index dc28b56e74..1a904c2b7c 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -22,6 +22,11 @@
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
 #include "optimizer/planmain.h"
+#include "catalog/pg_operator.h"
+#include "optimizer/tlist.h"
+#include "utils/fmgroids.h"
+#include "nodes/makefuncs.h"
+#include "utils/lsyscache.h"
 
 /* Hook for plugins to get control in add_paths_to_joinrel() */
 set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
@@ -195,7 +200,7 @@ add_paths_to_joinrel(PlannerInfo *root,
 	 * way of implementing a full outer join, so override enable_mergejoin if
 	 * it's a full join.
 	 */
-	if (enable_mergejoin || jointype == JOIN_FULL)
+	if (enable_mergejoin || jointype == JOIN_FULL || jointype == JOIN_TEMPORAL_NORMALIZE)
 		extra.mergeclause_list = select_mergejoin_clauses(root,
 														  joinrel,
 														  outerrel,
@@ -937,6 +942,80 @@ sort_inner_and_outer(PlannerInfo *root,
 		Assert(inner_path);
 		jointype = JOIN_INNER;
 	}
+	else if (jointype == JOIN_TEMPORAL_NORMALIZE)
+	{
+		/*
+		 * outer_path is just sort; inner_path is append of (B, ts) projection with (B, te) projection
+		 */
+		List *exprs = NIL; // to collect inner relation's targets
+		FuncExpr *f_split;
+		Var *innervar;
+
+		foreach(l, extra->mergeclause_list)
+		{
+			RestrictInfo       *rinfo = (RestrictInfo *) lfirst(l);
+			Expr               *clause  = (Expr *) rinfo->clause;
+
+			if (IsA(clause, OpExpr))
+			{
+				OpExpr *opexpr = (OpExpr *) clause;
+				if (opexpr->opno == OID_RANGE_EQ_OP)
+				{
+					if (IsA(lsecond(opexpr->args), Var)) {
+
+						// lsecond because it is from the second relation (=inner)
+						innervar = lsecond(opexpr->args);
+
+						f_split = makeFuncExpr(F_RANGE_SPLIT, 23, list_make1(innervar), 0, 0, 0);
+						f_split->funcretset = true;
+
+						/*
+						 * OUTER_VAR cannot be used here, because path creation does not know about it,
+						 * it will be introduced in plan creation.
+						 */
+						innervar = makeVar(2, innervar->varattno, f_split->funcresulttype, -1, 0, 0);
+					}
+				}
+				else
+				{
+					// lsecond because it is from the second relation (=inner)
+					exprs = lappend(exprs, lsecond(opexpr->args));
+				}
+			}
+		}
+
+		RestrictInfo       *rinfo = (RestrictInfo *) linitial(extra->mergeclause_list);
+		OpExpr *opexpr = (OpExpr *) rinfo->clause;
+		lsecond(opexpr->args) = f_split;
+		rinfo->right_em->em_expr = f_split;
+		rinfo->mergeopfamilies = get_mergejoin_opfamilies(opexpr->opno);
+
+		PathTarget *target_split = makeNode(PathTarget);
+		target_split->exprs = lappend(exprs, f_split);
+
+		set_pathtarget_cost_width(root, target_split);
+
+		inner_path = (Path *) create_set_projection_path(root, innerrel, inner_path, target_split);
+		innerrel->reltarget->exprs = inner_path->pathtarget->exprs;//list_make1(innervar);//copyObject(inner_path->pathtarget->exprs);
+		joinrel->reltarget->exprs = list_concat(copyObject(outerrel->reltarget->exprs), innerrel->reltarget->exprs);
+		set_pathtarget_cost_width(root, joinrel->reltarget);
+
+		innerrel->cheapest_total_path = inner_path;
+		innerrel->cheapest_startup_path = inner_path;
+		innerrel->cheapest_parameterized_paths = inner_path;
+		innerrel->pathlist = list_make1(inner_path);
+
+		extra->sjinfo->semi_rhs_exprs = list_make1(f_split);
+		extra->sjinfo->semi_operators = NIL;
+		extra->sjinfo->semi_operators = lappend_oid(extra->sjinfo->semi_operators, 96);
+
+		Assert(inner_path);
+
+		innerrel->cheapest_total_path = inner_path;
+		innerrel->cheapest_startup_path = inner_path;
+		innerrel->cheapest_parameterized_paths = inner_path;
+	}
+
 
 	/*
 	 * If the joinrel is parallel-safe, we may be able to consider a partial
@@ -1028,6 +1107,16 @@ sort_inner_and_outer(PlannerInfo *root,
 		merge_pathkeys = build_join_pathkeys(root, joinrel, jointype,
 											 outerkeys);
 
+		if (jointype == JOIN_TEMPORAL_NORMALIZE)
+		{
+			inner_path = (Path *) create_sort_path(root, innerrel, inner_path, innerkeys, -1);
+			innerrel->cheapest_total_path = inner_path;
+			innerrel->cheapest_startup_path = inner_path;
+			innerrel->cheapest_parameterized_paths = inner_path;
+			innerrel->pathlist = list_make1(inner_path);
+			Assert(inner_path);
+		}
+
 		/*
 		 * And now we can make the path.
 		 *
@@ -1360,6 +1449,7 @@ match_unsorted_outer(PlannerInfo *root,
 			break;
 		case JOIN_RIGHT:
 		case JOIN_FULL:
+		case JOIN_TEMPORAL_NORMALIZE:
 			nestjoinOK = false;
 			useallclauses = true;
 			break;
@@ -1686,6 +1776,10 @@ hash_inner_and_outer(PlannerInfo *root,
 	List	   *hashclauses;
 	ListCell   *l;
 
+	/* Hashjoin is not allowed for temporal NORMALIZE */
+	if (jointype == JOIN_TEMPORAL_NORMALIZE)
+		return;
+
 	/*
 	 * We need to build only one hashclauses list for any given pair of outer
 	 * and inner relations; all of the hashable clauses will be used as keys.
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 6a480ab764..c7045b5fe7 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -912,6 +912,21 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1,
 								 JOIN_ANTI, sjinfo,
 								 restrictlist);
 			break;
+		case JOIN_TEMPORAL_NORMALIZE:
+			if (is_dummy_rel(rel1) || is_dummy_rel(rel2) ||
+				restriction_is_constant_false(restrictlist, joinrel, false))
+			{
+				mark_dummy_rel(joinrel);
+				break;
+			}
+
+			/*
+			 * Temporal normalization does not support re-ordering of rels
+			 */
+			add_paths_to_joinrel(root, joinrel, rel1, rel2,
+								 JOIN_TEMPORAL_NORMALIZE, sjinfo,
+								 restrictlist);
+			break;
 		default:
 			/* other values not expected here */
 			elog(ERROR, "unrecognized join type: %d", (int) sjinfo->jointype);
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index 2f4fea241a..6309ebe9fd 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -1030,7 +1030,12 @@ build_join_pathkeys(PlannerInfo *root,
 					JoinType jointype,
 					List *outer_pathkeys)
 {
-	if (jointype == JOIN_FULL || jointype == JOIN_RIGHT)
+	/*
+	 * TEMPORAL NORMALIZE: To improve this, we would need to remove only
+	 * temporal range types from the path key list, not all
+	 */
+	if (jointype == JOIN_FULL || jointype == JOIN_RIGHT
+			|| jointype == JOIN_TEMPORAL_NORMALIZE)
 		return NIL;
 
 	/*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c6b8553a08..3a09e3677f 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -4365,6 +4365,15 @@ create_mergejoin_plan(PlannerInfo *root,
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
+	/*
+	 * XXX PEMOSER NORMALIZE needs a result node above to properly
+	 * handle target lists, functions and constants
+	 * Maybe we need to refine this like in create_unique_plan:
+	 * "If the top plan node can't do projections..."
+	 */
+	if (best_path->jpath.jointype == JOIN_TEMPORAL_NORMALIZE)
+		join_plan = make_result(tlist, NULL, join_plan);
+
 	return join_plan;
 }
 
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 73da0c2d8e..9fec2a6c81 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -33,6 +33,7 @@
 #include "parser/analyze.h"
 #include "rewrite/rewriteManip.h"
 #include "utils/lsyscache.h"
+#include "catalog/pg_operator.h"
 
 
 /* These parameters are set by GUC */
@@ -870,6 +871,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
 		switch (j->jointype)
 		{
 			case JOIN_INNER:
+			case JOIN_TEMPORAL_NORMALIZE:
 				leftjoinlist = deconstruct_recurse(root, j->larg,
 												   below_outer_join,
 												   &leftids, &left_inners,
@@ -992,7 +994,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
 										*inner_join_rels,
 										j->jointype,
 										my_quals);
-			if (j->jointype == JOIN_SEMI)
+			if (j->jointype == JOIN_SEMI || j->jointype == JOIN_TEMPORAL_NORMALIZE)
 				ojscope = NULL;
 			else
 				ojscope = bms_union(sjinfo->min_lefthand,
@@ -1411,7 +1413,8 @@ compute_semijoin_info(SpecialJoinInfo *sjinfo, List *clause)
 	sjinfo->semi_rhs_exprs = NIL;
 
 	/* Nothing more to do if it's not a semijoin */
-	if (sjinfo->jointype != JOIN_SEMI)
+	if (sjinfo->jointype != JOIN_SEMI
+			&& sjinfo->jointype != JOIN_TEMPORAL_NORMALIZE)
 		return;
 
 	/*
@@ -2595,6 +2598,10 @@ check_mergejoinable(RestrictInfo *restrictinfo)
 	opno = ((OpExpr *) clause)->opno;
 	leftarg = linitial(((OpExpr *) clause)->args);
 
+	// XXX PEMOSER Hardcoded NORMALIZE detection... change this. Read the note below...
+	if (opno == OID_RANGE_EQ_OP)
+		restrictinfo->temp_normalizer = true;
+
 	if (op_mergejoinable(opno, exprType(leftarg)) &&
 		!contain_volatile_functions((Node *) clause))
 		restrictinfo->mergeopfamilies = get_mergejoin_opfamilies(opno);
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 4fbc03fe54..27f0dea78c 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -780,6 +780,7 @@ pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
 		switch (j->jointype)
 		{
 			case JOIN_INNER:
+			case JOIN_TEMPORAL_NORMALIZE:
 				j->larg = pull_up_subqueries_recurse(root, j->larg,
 													 lowest_outer_join,
 													 lowest_nulling_outer_join,
diff --git a/src/backend/optimizer/util/restrictinfo.c b/src/backend/optimizer/util/restrictinfo.c
index 3b50fd29ad..0a5a2736f6 100644
--- a/src/backend/optimizer/util/restrictinfo.c
+++ b/src/backend/optimizer/util/restrictinfo.c
@@ -188,6 +188,7 @@ make_restrictinfo_internal(Expr *clause,
 	restrictinfo->outer_selec = -1;
 
 	restrictinfo->mergeopfamilies = NIL;
+	restrictinfo->temp_normalizer = false;
 
 	restrictinfo->left_ec = NULL;
 	restrictinfo->right_ec = NULL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c97bb367f8..a148684f92 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -438,6 +438,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	join_outer join_qual
 %type <jtype>	join_type
 
+%type <node>    normalizer_qual
+%type <jexpr>   normalized_table
+%type <list>	temporal_bounds temporal_bounds_list
+
 %type <list>	extract_list overlay_list position_list
 %type <list>	substr_list trim_list
 %type <list>	opt_interval interval_second
@@ -659,7 +663,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
 
@@ -11930,6 +11934,11 @@ table_ref:	relation_expr opt_alias_clause
 					$2->alias = $4;
 					$$ = (Node *) $2;
 				}
+			| '(' normalized_table ')' alias_clause
+				{
+					$2->alias = $4;
+					$$ = (Node *) $2;
+				}
 		;
 
 
@@ -12048,6 +12057,59 @@ opt_alias_clause: alias_clause						{ $$ = $1; }
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+/*
+ * Temporal alignment statements
+ */
+temporal_bounds: WITH '(' temporal_bounds_list ')'				{ $$ = $3; }
+		;
+
+temporal_bounds_list:
+			columnref
+				{
+					$$ = list_make1($1);
+				}
+			| temporal_bounds_list ',' columnref
+				{
+					$$ = lappend($1, $3);
+				}
+		;
+
+normalizer_qual:
+			USING '(' name_list ')'					{ $$ = (Node *) $3; }
+			| USING '(' ')'							{ $$ = (Node *) NIL; }
+		;
+
+normalized_table:
+			table_ref NORMALIZE table_ref normalizer_qual temporal_bounds
+				{
+					JoinExpr *n = makeNode(JoinExpr);
+					n->jointype = JOIN_TEMPORAL_NORMALIZE;
+					n->isNatural = false;
+					n->larg = $1;
+					n->rarg = $3;
+
+					n->usingClause = NIL;
+
+					if ($4 != NULL && IsA($4, List))
+						n->usingClause = (List *) $4; /* USING clause */
+
+					/*
+					 * A list for our valid-time boundaries with two range typed
+					 * values.
+					 */
+					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;
+				}
+		;
+
 /*
  * func_alias_clause can include both an Alias and a coldeflist, so we make it
  * return a 2-element list that gets disassembled by calling production.
@@ -15466,6 +15528,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 2a6b2ff153..8c61d1c615 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -61,7 +61,7 @@ static void extractRemainingColumns(List *common_colnames,
 									List **res_colnames, List **res_colvars);
 static Node *transformJoinUsingClause(ParseState *pstate,
 									  RangeTblEntry *leftRTE, RangeTblEntry *rightRTE,
-									  List *leftVars, List *rightVars);
+									  List *leftVars, List *rightVars, List *normalizeVars);
 static Node *transformJoinOnClause(ParseState *pstate, JoinExpr *j,
 								   List *namespace);
 static RangeTblEntry *getRTEForSpecialRelationTypes(ParseState *pstate,
@@ -299,7 +299,7 @@ extractRemainingColumns(List *common_colnames,
 static Node *
 transformJoinUsingClause(ParseState *pstate,
 						 RangeTblEntry *leftRTE, RangeTblEntry *rightRTE,
-						 List *leftVars, List *rightVars)
+						 List *leftVars, List *rightVars, List *normalizeVars)
 {
 	Node	   *result;
 	List	   *andargs = NIL;
@@ -333,6 +333,17 @@ transformJoinUsingClause(ParseState *pstate,
 		andargs = lappend(andargs, e);
 	}
 
+	/* Temporal NORMALIZE appends an expression to compare temporal bounds */
+	if (normalizeVars)
+	{
+		A_Expr *e;
+		e = makeSimpleA_Expr(AEXPR_OP, "=",
+							 (Node *) copyObject(linitial(normalizeVars)),
+							 (Node *) copyObject(lsecond(normalizeVars)),
+							 -1);
+		andargs = lappend(andargs, e);
+	}
+
 	/* Only need an AND if there's more than one join column */
 	if (list_length(andargs) == 1)
 		result = (Node *) linitial(andargs);
@@ -1193,6 +1204,7 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		int			sv_namespace_length;
 		RangeTblEntry *rte;
 		int			k;
+		int         isNormalize = (j->jointype == JOIN_TEMPORAL_NORMALIZE);
 
 		/*
 		 * Recursively process the left subtree, then the right.  We must do
@@ -1303,7 +1315,8 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		res_colnames = NIL;
 		res_colvars = NIL;
 
-		if (j->usingClause)
+		/* NORMALIZE supports empty using clauses */
+		if (j->usingClause || (isNormalize && j->usingClause == NIL))
 		{
 			/*
 			 * JOIN/USING (or NATURAL JOIN, as transformed above). Transform
@@ -1313,6 +1326,7 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 			List	   *ucols = j->usingClause;
 			List	   *l_usingvars = NIL;
 			List	   *r_usingvars = NIL;
+			List       *normalize_vars = NIL;
 			ListCell   *ucol;
 
 			Assert(j->quals == NULL);	/* shouldn't have ON() too */
@@ -1398,11 +1412,90 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 														 r_colvar));
 			}
 
+			/* Only for temporal NORMALIZE */
+			if (isNormalize)
+			{
+				int         ndx = 0;
+				ListCell   *col;
+				Var        *l_boundvar;
+				Var        *r_boundvar;
+
+				int         l_bound_index = -1;
+				int         r_bound_index = -1;
+				char       *l_bound;
+				char       *r_bound;
+				ListCell *lboundcol = linitial(((ColumnRef *)linitial(j->temporalBounds))->fields);
+				ListCell *rboundcol = linitial(((ColumnRef *)lsecond(j->temporalBounds))->fields);
+
+				l_bound = strVal(lboundcol);
+				r_bound = strVal(rboundcol);
+
+				/* Find the first bound in left input */
+				foreach(col, l_colnames)
+				{
+					char       *l_colname = strVal(lfirst(col));
+
+					if (strcmp(l_colname, l_bound) == 0)
+					{
+						if (l_bound_index >= 0)
+							ereport(ERROR,
+									(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+											 errmsg("temporal bound name \"%s\" appears more than once in left table",
+													l_bound)));
+						l_bound_index = ndx;
+					}
+					ndx++;
+				}
+
+				if (l_bound_index < 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_COLUMN),
+									 errmsg("column \"%s\" specified in normalizer's WITH clause does not exist in left table",
+											l_bound)));
+
+				/* Find the second bound in right input */
+				ndx = 0;
+				foreach(col, r_colnames)
+				{
+					char       *r_colname = strVal(lfirst(col));
+
+					if (strcmp(r_colname, r_bound) == 0)
+					{
+						if (r_bound_index >= 0)
+							ereport(ERROR,
+									(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+											 errmsg("temporal bound name \"%s\" appears more than once in right table",
+													l_bound)));
+						r_bound_index = ndx;
+					}
+					ndx++;
+				}
+
+				if (r_bound_index < 0)
+					ereport(ERROR,
+							(errcode(ERRCODE_UNDEFINED_COLUMN),
+									 errmsg("column \"%s\" specified in normalizer's WITH clause does not exist in right table",
+											r_bound)));
+
+				l_boundvar = list_nth(l_colvars, l_bound_index);
+				normalize_vars = lappend(normalize_vars, l_boundvar);
+				r_boundvar = list_nth(r_colvars, r_bound_index);
+				normalize_vars = lappend(normalize_vars, r_boundvar);
+
+				res_colnames = lappend(res_colnames, lboundcol);
+				res_colvars = lappend(res_colvars,
+									  buildMergedJoinVar(pstate,
+														 j->jointype,
+														 l_boundvar,
+														 r_boundvar));
+			}
+
 			j->quals = transformJoinUsingClause(pstate,
 												l_rte,
 												r_rte,
 												l_usingvars,
-												r_usingvars);
+												r_usingvars,
+												normalize_vars);
 		}
 		else if (j->quals)
 		{
@@ -1418,13 +1511,21 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		extractRemainingColumns(res_colnames,
 								l_colnames, l_colvars,
 								&l_colnames, &l_colvars);
-		extractRemainingColumns(res_colnames,
-								r_colnames, r_colvars,
-								&r_colnames, &r_colvars);
+
+		//Temporal normalizers expose only outer relation's columns...
+		if (!isNormalize)
+			extractRemainingColumns(res_colnames,
+									r_colnames, r_colvars,
+									&r_colnames, &r_colvars);
+
 		res_colnames = list_concat(res_colnames, l_colnames);
 		res_colvars = list_concat(res_colvars, l_colvars);
-		res_colnames = list_concat(res_colnames, r_colnames);
-		res_colvars = list_concat(res_colvars, r_colvars);
+
+		if (!isNormalize)
+		{
+			res_colnames = list_concat(res_colnames, r_colnames);
+			res_colvars = list_concat(res_colvars, r_colvars);
+		}
 
 		/*
 		 * Check alias (AS clause), if any.
@@ -1567,6 +1668,7 @@ buildMergedJoinVar(ParseState *pstate, JoinType jointype,
 	switch (jointype)
 	{
 		case JOIN_INNER:
+		case JOIN_TEMPORAL_NORMALIZE:
 
 			/*
 			 * We can use either var; prefer non-coerced one if available.
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index e5c7e5c7ee..ac5e1b4c58 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -41,6 +41,7 @@
 #include "utils/lsyscache.h"
 #include "utils/rangetypes.h"
 #include "utils/timestamp.h"
+#include "funcapi.h"
 
 
 #define RANGE_EMPTY_LITERAL "empty"
@@ -467,6 +468,79 @@ range_upper(PG_FUNCTION_ARGS)
 	PG_RETURN_DATUM(upper.val);
 }
 
+/* split lower/upper bound into two rows of data */
+Datum
+range_split(PG_FUNCTION_ARGS)
+{
+	typedef struct
+	{
+		RangeBound	lower;
+		RangeBound	upper;
+		bool		empty;
+	} RangeSplitFuncContext;
+
+	FuncCallContext *funcctx;
+	MemoryContext oldcontext;
+	RangeSplitFuncContext *fctx;
+
+	/* stuff done only on the first call of the function */
+	if (SRF_IS_FIRSTCALL())
+	{
+		RangeType  		*r1;
+		TypeCacheEntry  *typcache;
+
+		/* create a function context for cross-call persistence */
+		funcctx = SRF_FIRSTCALL_INIT();
+
+		/*
+		 * switch to memory context appropriate for multiple function calls
+		 */
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		r1 = PG_GETARG_RANGE_P(0);
+
+		/* allocate memory for user context */
+		fctx = (RangeSplitFuncContext *) palloc(sizeof(RangeSplitFuncContext));
+
+		/*
+		 * We cannot use range_get_typecache, because it would overwrite
+		 * fcinfo->flinfo->fn_extra
+		 */
+		typcache = lookup_type_cache(RangeTypeGetOid(r1), TYPECACHE_RANGE_INFO);
+		if (typcache->rngelemtype == NULL)
+			elog(ERROR, "type %u is not a range type", RangeTypeGetOid(r1));
+		range_deserialize(typcache, r1, &fctx->lower, &fctx->upper, &fctx->empty);
+
+		funcctx->user_fctx = fctx;
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	/* stuff done on every call of the function */
+	funcctx = SRF_PERCALL_SETUP();
+	fctx = (RangeSplitFuncContext *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr == 0)
+	{
+		/* Return NULL if there's no finite lower bound */
+		if (fctx->empty || fctx->lower.infinite)
+			SRF_RETURN_NEXT_NULL(funcctx);
+
+		SRF_RETURN_NEXT(funcctx, fctx->lower.val);
+	}
+
+	if (funcctx->call_cntr == 1)
+	{
+		/* Return NULL if there's no finite upper bound */
+		if (fctx->empty || fctx->upper.infinite)
+			SRF_RETURN_NEXT_NULL(funcctx);
+
+		SRF_RETURN_NEXT(funcctx, fctx->upper.val);
+	}
+
+	/* done, after extracting lower and upper bounds */
+	SRF_RETURN_DONE(funcctx);
+}
+
 
 /* range -> bool functions */
 
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 7eba59eff3..33998ea133 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -2065,6 +2065,7 @@ eqjoinsel(PG_FUNCTION_ARGS)
 		case JOIN_INNER:
 		case JOIN_LEFT:
 		case JOIN_FULL:
+		case JOIN_TEMPORAL_NORMALIZE:
 			selec = selec_inner;
 			break;
 		case JOIN_SEMI:
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 96823cd59b..32fd599739 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3086,7 +3086,7 @@
   oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' },
 
 # generic range type operators
-{ oid => '3882', descr => 'equal',
+{ oid => '3882', descr => 'equal', oid_symbol => 'OID_RANGE_EQ_OP',
   oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'anyrange',
   oprright => 'anyrange', oprresult => 'bool', oprcom => '=(anyrange,anyrange)',
   oprnegate => '<>(anyrange,anyrange)', oprcode => 'range_eq',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0902dce5f1..5db2d75c88 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -9572,6 +9572,10 @@
 { oid => '3867',
   proname => 'range_union', prorettype => 'anyrange',
   proargtypes => 'anyrange anyrange', prosrc => 'range_union' },
+{ oid => '4001',
+  descr => 'lower and upper bound of range returned as two tuples',
+  proname => 'range_split', prorettype => 'anyelement', proretset => 't',
+  proargtypes => 'anyrange', prosrc => 'range_split' },
 { oid => '4057',
   descr => 'the smallest range which includes both of the given ranges',
   proname => 'range_merge', prorettype => 'anyrange',
diff --git a/src/include/executor/execdebug.h b/src/include/executor/execdebug.h
index c119fdf4fa..1dd3b2d513 100644
--- a/src/include/executor/execdebug.h
+++ b/src/include/executor/execdebug.h
@@ -82,6 +82,7 @@
  *		sort node debugging defines
  * ----------------
  */
+#define EXEC_SORTDEBUG
 #ifdef EXEC_SORTDEBUG
 #define SO_nodeDisplay(l)				nodeDisplay(l)
 #define SO_printf(s)					printf(s)
@@ -96,14 +97,15 @@
  *		merge join debugging defines
  * ----------------
  */
+#define EXEC_MERGEJOINDEBUG
 #ifdef EXEC_MERGEJOINDEBUG
 
 #define MJ_nodeDisplay(l)				nodeDisplay(l)
-#define MJ_printf(s)					printf(s)
-#define MJ1_printf(s, p)				printf(s, p)
-#define MJ2_printf(s, p1, p2)			printf(s, p1, p2)
-#define MJ_debugtup(slot)				debugtup(slot, NULL)
-#define MJ_dump(state)					ExecMergeTupleDump(state)
+#define MJ_printf(s)					printf(s); fflush(stdout)
+#define MJ1_printf(s, p)				printf(s, p); fflush(stdout)
+#define MJ2_printf(s, p1, p2)			printf(s, p1, p2); fflush(stdout)
+#define MJ_debugtup(slot)				debugtup(slot, NULL); fflush(stdout)
+#define MJ_dump(state)					ExecMergeTupleDump(state); fflush(stdout)
 #define MJ_DEBUG_COMPARE(res) \
   MJ1_printf("  MJCompare() returns %d\n", (res))
 #define MJ_DEBUG_QUAL(clause, res) \
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 98bdcbcef5..6146e52faf 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1845,6 +1845,17 @@ typedef struct MergeJoinState
 	TupleTableSlot *mj_NullInnerTupleSlot;
 	ExprContext *mj_OuterEContext;
 	ExprContext *mj_InnerEContext;
+
+	/* needed by temporal normalization */
+	bool				 mj_markSet;
+	bool				 mj_isNormalizer;
+	bool				 sameleft;
+	bool				*nullMask;		/* See heap_modify_tuple */
+	bool				*tsteMask;		/* See heap_modify_tuple */
+	Datum				*newValues;		/* tuple values that get updated */
+	Datum				 sweepline;
+	Form_pg_attribute	 datumFormat;	/* Datum format of sweepline, P1, P2 */
+	TupleTableSlot 		*prev;
 } MergeJoinState;
 
 /* ----------------
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 4e2fb39105..6542af6188 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -84,6 +84,7 @@ typedef enum NodeTag
 	T_SetOp,
 	T_LockRows,
 	T_Limit,
+	T_TemporalAdjustment,
 	/* these aren't subclasses of Plan: */
 	T_NestLoopParam,
 	T_PlanRowMark,
@@ -140,6 +141,7 @@ typedef enum NodeTag
 	T_SetOpState,
 	T_LockRowsState,
 	T_LimitState,
+	T_TemporalAdjustmentState,
 
 	/*
 	 * TAGS FOR PRIMITIVE NODES (primnodes.h)
@@ -256,6 +258,7 @@ typedef enum NodeTag
 	T_LockRowsPath,
 	T_ModifyTablePath,
 	T_LimitPath,
+	T_TemporalAdjustmentPath,
 	/* these aren't subclasses of Path: */
 	T_EquivalenceClass,
 	T_EquivalenceMember,
@@ -476,6 +479,7 @@ typedef enum NodeTag
 	T_PartitionRangeDatum,
 	T_PartitionCmd,
 	T_VacuumRelation,
+	T_TemporalClause,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
@@ -716,7 +720,12 @@ 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
+	 */
+	JOIN_TEMPORAL_NORMALIZE
 
 	/*
 	 * We might need additional join types someday.
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index e3c579ee44..75b5730b57 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2001,6 +2001,7 @@ typedef struct RestrictInfo
 
 	/* valid if clause is hashjoinable, else InvalidOid: */
 	Oid			hashjoinoperator;	/* copy of clause operator */
+	bool        temp_normalizer;
 
 	/* cache space for hashclause processing; -1 if not yet set */
 	Selectivity left_bucketsize;	/* avg bucketsize of left side */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 860a84de7c..3e43246a2b 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -507,6 +507,7 @@ typedef struct OpExpr
 	Oid			inputcollid;	/* OID of collation that operator should use */
 	List	   *args;			/* arguments to the operator (1 or 2) */
 	int			location;		/* token location, or -1 if unknown */
+	bool		isnormalize;
 } OpExpr;
 
 /*
@@ -1479,6 +1480,8 @@ 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 */
 } JoinExpr;
 
 /*----------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 00ace8425e..ecb5d7a473 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -261,6 +261,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)
#65Peter Moser
pitiz29a@gmail.com
In reply to: Ibrar Ahmed (#64)
Re: [PROPOSAL] Temporal query processing with range types

Hi Ibrar, Thomas and Robert,

On 8/2/19 11:00 PM, Ibrar Ahmed wrote:

I have rebased the patch and currently reviewing the patch 
on master (1e2fddfa33d3c7cc93ca3ee0f32852699bd3e012).

Thanks a lot for your effort. We are now trying to put again more work
and time in this patch.
We are grateful for any feedback.

Thanks,
Peter

#66Michael Paquier
michael@paquier.xyz
In reply to: Peter Moser (#65)
Re: [PROPOSAL] Temporal query processing with range types

On Thu, Aug 08, 2019 at 09:58:31AM +0200, Peter Moser wrote:

Thanks a lot for your effort. We are now trying to put again more work
and time in this patch.
We are grateful for any feedback.

The latest patch applies, but does not build because of an OID
conflict. For development purposes, please make sure to use an OID in
the range 8000~9000 which are reserved for development per the
recently-added new project policy. For now I have moved the patch to
next CF, waiting on author.
--
Michael

#67Peter Moser
pitiz29a@gmail.com
In reply to: Michael Paquier (#66)
Re: [PROPOSAL] Temporal query processing with range types

Hi Hackers,

On 12/1/19 4:45 AM, Michael Paquier wrote:

For now I have moved the patch to
next CF, waiting on author.

We have withdrawn this patch for now. The reason for this is, that we
had ideas on how to split it into multiple simpler independent patches,
that can be reviewed and committed one by one.

Best regards,
Anton and Peter