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