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