Thanks for the detailed feedback, I'm sorry it took so long to
incorporate it. I've attached the latest version of the patch, fixing
in particular:
> We have this block:
I've re-written this so it only does a single pass through the window
definitions (my patch originally added a second pass), and only does
the clone if required.
> In gram.y there are some spurious whitespaces at end-of-line.
Fixed - I didn't know about diff --check, it's very useful!
> Also, in parsenodes.h, you had the [MANDATORY] and such tags.
I've re-written the comments (without tags) to make it much easier to
understand . I agree they were ugly!
>Exactly what case does the "in this case" phrase refer to?
Clarified in the comments
>A style issue. You have this:
Fixed
> And a final style comment.
Fixed
> Finally, I'm not really sure about the column you added to the regression
> tests table.
Indeed, it was a bit artificial. I've re-written the tests to use a
separate table as you suggest.
Thanks -
Nick
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 0809a6d..5da852e 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -13185,6 +13185,7 @@ SELECT xmlagg(x) FROM (SELECT x FROM test ORDER BY y DESC) AS tab;
lag(<replaceable class="parameter">value</replaceable> <type>any</>
[, <replaceable class="parameter">offset</replaceable> <type>integer</>
[, <replaceable class="parameter">default</replaceable> <type>any</> ]])
+ [ { RESPECT | IGNORE } NULLS ]
</function>
</entry>
<entry>
@@ -13199,7 +13200,9 @@ SELECT xmlagg(x) FROM (SELECT x FROM test ORDER BY y DESC) AS tab;
<replaceable class="parameter">default</replaceable> are evaluated
with respect to the current row. If omitted,
<replaceable class="parameter">offset</replaceable> defaults to 1 and
- <replaceable class="parameter">default</replaceable> to null
+ <replaceable class="parameter">default</replaceable> to null. If
+ <literal>IGNORE NULLS</> is specified then the function will be evaluated
+ as if the rows containing nulls didn't exist.
</entry>
</row>
@@ -13212,6 +13215,7 @@ SELECT xmlagg(x) FROM (SELECT x FROM test ORDER BY y DESC) AS tab;
lead(<replaceable class="parameter">value</replaceable> <type>any</>
[, <replaceable class="parameter">offset</replaceable> <type>integer</>
[, <replaceable class="parameter">default</replaceable> <type>any</> ]])
+ [ { RESPECT | IGNORE } NULLS ]
</function>
</entry>
<entry>
@@ -13226,7 +13230,9 @@ SELECT xmlagg(x) FROM (SELECT x FROM test ORDER BY y DESC) AS tab;
<replaceable class="parameter">default</replaceable> are evaluated
with respect to the current row. If omitted,
<replaceable class="parameter">offset</replaceable> defaults to 1 and
- <replaceable class="parameter">default</replaceable> to null
+ <replaceable class="parameter">default</replaceable> to null. If
+ <literal>IGNORE NULLS</> is specified then the function will be evaluated
+ as if the rows containing nulls didn't exist.
</entry>
</row>
@@ -13320,11 +13326,10 @@ SELECT xmlagg(x) FROM (SELECT x FROM test ORDER BY y DESC) AS tab;
<note>
<para>
The SQL standard defines a <literal>RESPECT NULLS</> or
- <literal>IGNORE NULLS</> option for <function>lead</>, <function>lag</>,
- <function>first_value</>, <function>last_value</>, and
- <function>nth_value</>. This is not implemented in
- <productname>PostgreSQL</productname>: the behavior is always the
- same as the standard's default, namely <literal>RESPECT NULLS</>.
+ <literal>IGNORE NULLS</> option for <function>first_value</>,
+ <function>last_value</>, and <function>nth_value</>. This is not
+ implemented in <productname>PostgreSQL</productname>: the behavior is
+ always the same as the standard's default, namely <literal>RESPECT NULLS</>.
Likewise, the standard's <literal>FROM FIRST</> or <literal>FROM LAST</>
option for <function>nth_value</> is not implemented: only the
default <literal>FROM FIRST</> behavior is supported. (You can achieve
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 2fcc630..5cea825 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2431,7 +2431,6 @@ window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot)
* API exposed to window functions
***********************************************************************/
-
/*
* WinGetPartitionLocalMemory
* Get working memory that lives till end of partition processing
@@ -2467,6 +2466,17 @@ WinGetCurrentPosition(WindowObject winobj)
}
/*
+ * WinGetFrameOptions
+ * Returns the frame option flags
+ */
+int
+WinGetFrameOptions(WindowObject winobj)
+{
+ Assert(WindowObjectIsValid(winobj));
+ return winobj->winstate->frameOptions;
+}
+
+/*
* WinGetPartitionRowCount
* Return total number of rows contained in the current partition.
*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7b9895d..f11bc66 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -290,6 +290,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <list> TriggerEvents TriggerOneEvent
%type <value> TriggerFuncArg
%type <node> TriggerWhen
+%type <ival> opt_ignore_nulls
%type <list> event_trigger_when_list event_trigger_value_list
%type <defelt> event_trigger_when_item
@@ -552,7 +553,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
HANDLER HAVING HEADER_P HOLD HOUR_P
- IDENTITY_P IF_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IN_P
+ IDENTITY_P IF_P IGNORE ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IN_P
INCLUDING INCREMENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P
INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
@@ -582,7 +583,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
RANGE READ REAL REASSIGN RECHECK RECURSIVE REF REFERENCES REFRESH REINDEX
RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA
- RESET RESTART RESTRICT RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK
+ RESET RESPECT RESTART RESTRICT RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK
ROW ROWS RULE
SAVEPOINT SCHEMA SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES
@@ -11885,19 +11886,28 @@ window_definition:
}
;
-over_clause: OVER window_specification
- { $$ = $2; }
- | OVER ColId
+opt_ignore_nulls:
+ IGNORE NULLS_P { $$ = FRAMEOPTION_IGNORE_NULLS; }
+ | RESPECT NULLS_P { $$ = 0; }
+ | /* EMPTY */ { $$ = 0; }
+ ;
+
+over_clause: opt_ignore_nulls OVER window_specification
+ {
+ $3->frameOptions |= $1;
+ $$ = $3;
+ }
+ | opt_ignore_nulls OVER ColId
{
WindowDef *n = makeNode(WindowDef);
- n->name = $2;
+ n->name = $3;
n->refname = NULL;
n->partitionClause = NIL;
n->orderClause = NIL;
- n->frameOptions = FRAMEOPTION_DEFAULTS;
+ n->frameOptions = FRAMEOPTION_DEFAULTS | $1;
n->startOffset = NULL;
n->endOffset = NULL;
- n->location = @2;
+ n->location = @3;
$$ = n;
}
| /*EMPTY*/
@@ -12891,6 +12901,7 @@ unreserved_keyword:
| HOUR_P
| IDENTITY_P
| IF_P
+ | IGNORE
| IMMEDIATE
| IMMUTABLE
| IMPLICIT_P
@@ -12980,6 +12991,7 @@ unreserved_keyword:
| REPLACE
| REPLICA
| RESET
+ | RESPECT
| RESTART
| RESTRICT
| RETURNS
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 272d27f..6edfc4d 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -694,28 +694,82 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
{
Index winref = 0;
ListCell *lc;
+ WindowDef *refwin = NULL;
Assert(windef->refname == NULL &&
windef->partitionClause == NIL &&
- windef->orderClause == NIL &&
- windef->frameOptions == FRAMEOPTION_DEFAULTS);
+ windef->orderClause == NIL);
foreach(lc, pstate->p_windowdefs)
{
- WindowDef *refwin = (WindowDef *) lfirst(lc);
-
+ WindowDef *thiswin = (WindowDef *) lfirst(lc);
winref++;
- if (refwin->name && strcmp(refwin->name, windef->name) == 0)
+
+ if (thiswin->name && strcmp(thiswin->name, windef->name) == 0)
+ {
+ /*
+ * "thiswin" is the window we want - but we have to tweak the
+ * definition slightly as some window options (e.g. IGNORE
+ * NULLS) can't be specified a standalone window definition;
+ * they can only be specified when invoking a window function
+ * over a window definition. However, we don't want to modify
+ * the window def itself (as that'll affect other window
+ * functions that use it - so if we need to make changes to it
+ * we'll clone refwin, change the clone and add the clone to
+ * the list of window definitions in pstate.
+ *
+ * There's one catch: what if a statement has two (or more)
+ * window function calls that reference the same window
+ * definition, and both have IGNORE NULLs? We don't want to
+ * add two modified definitions to pstate, so we'll only break
+ * if thiswin is an exact match - if not we'll keep looking for
+ * a window definition with the same name *and* same frame
+ * options.
+ */
+ wfunc->winref = winref;
+ refwin = thiswin;
+ if(windef->frameOptions == FRAMEOPTION_DEFAULTS)
+ break; /* don't need to clone, so just use this one */
+ }
+
+ if (refwin && /* we need to have found the parent window */
+ thiswin->refname &&
+ strcmp(thiswin->refname, windef->name) == 0 && /* it reference the right parent */
+ thiswin->frameOptions == windef->frameOptions)
{
+ /* found a clone window specification that we can re-use */
wfunc->winref = winref;
+ refwin = thiswin;
break;
}
}
- if (lc == NULL) /* didn't find it? */
+
+ if (refwin == NULL) /* didn't find it? */
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("window \"%s\" does not exist", windef->name),
parser_errposition(pstate, windef->location)));
+ else if (windef->frameOptions != refwin->frameOptions /* we can't use the clone */
+ && windef->frameOptions != FRAMEOPTION_DEFAULTS) /* we can't use the parent */
+ {
+ /*
+ * This means we've found the parent, but no clones (if there were
+ * any) had the correct frame options. We'll clone the parent we
+ * found (refwin), set the frame options we want and add the new
+ * clone to pstate:
+ */
+ WindowDef *clone = makeNode(WindowDef);
+
+ clone->name = NULL;
+ clone->refname = pstrdup(refwin->name);
+ clone->frameOptions = windef->frameOptions; /* Note windef! */
+ clone->startOffset = copyObject(refwin->startOffset);
+ clone->endOffset = copyObject(refwin->endOffset);
+ clone->location = refwin->location;
+
+ pstate->p_windowdefs = lappend(pstate->p_windowdefs, clone);
+ wfunc->winref = list_length(pstate->p_windowdefs);
+ }
}
else
{
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index cc46084..ce39955 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -726,6 +726,22 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
NameListToString(funcname)),
parser_errposition(pstate, location)));
+ if (over->frameOptions & FRAMEOPTION_IGNORE_NULLS)
+ {
+ /*
+ * As this is only implemented for the lead & lag window functions
+ * we'll filter out all aggregate functions too.
+ */
+ if (fdresult != FUNCDETAIL_WINDOWFUNC
+ || (strcmp("lead", strVal(llast(funcname))) != 0 &&
+ strcmp("lag", strVal(llast(funcname))) != 0))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("RESPECT NULLS is only implemented for the lead and lag window functions"),
+ parser_errposition(pstate, location))); }
+ }
+
/*
* ordered aggs not allowed in windows yet
*/
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index ea7b8c5..add4048 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -4916,11 +4916,16 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
bool needspace = false;
const char *sep;
ListCell *l;
+ size_t refname_len = 0;
+ int initial_buf_len = buf->len;
appendStringInfoChar(buf, '(');
if (wc->refname)
{
- appendStringInfoString(buf, quote_identifier(wc->refname));
+ const char *quoted_refname = quote_identifier(wc->refname);
+
+ refname_len = strlen(quoted_refname);
+ appendStringInfoString(buf, quoted_refname);
needspace = true;
}
/* partition clauses are always inherited, so only print if no refname */
@@ -5002,7 +5007,20 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
/* we will now have a trailing space; remove it */
buf->len--;
}
- appendStringInfoChar(buf, ')');
+
+ /*
+ * We'll tidy up the output slightly; if we've got a refname, but haven't
+ * overridden the partition-by, order-by or any of the frame flags
+ * relevant inside the window def's ()s, then we'll be left with
+ * "(<refname>)". We'll trim off the brackets in this case:
+ */
+ if (wc->refname && buf->len == initial_buf_len + refname_len + 1)
+ {
+ memcpy(buf->data + initial_buf_len, buf->data + initial_buf_len + 1, refname_len);
+ buf->len -= 1; /* the trailing ")" */
+ }
+ else
+ appendStringInfoChar(buf, ')');
}
/* ----------
@@ -7674,7 +7692,7 @@ get_windowfunc_expr(WindowFunc *wfunc, deparse_context *context)
get_rule_expr((Node *) wfunc->aggfilter, context, false);
}
- appendStringInfoString(buf, ") OVER ");
+ appendStringInfoString(buf, ") ");
foreach(l, context->windowClause)
{
@@ -7682,6 +7700,10 @@ get_windowfunc_expr(WindowFunc *wfunc, deparse_context *context)
if (wc->winref == wfunc->winref)
{
+ if (wc->frameOptions & FRAMEOPTION_IGNORE_NULLS)
+ appendStringInfoString(buf, "IGNORE NULLS ");
+ appendStringInfoString(buf, "OVER ");
+
if (wc->name)
appendStringInfoString(buf, quote_identifier(wc->name));
else
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 19f1fde..ccb53ce 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -13,7 +13,9 @@
*/
#include "postgres.h"
+#include "nodes/bitmapset.h"
#include "utils/builtins.h"
+#include "utils/memutils.h"
#include "windowapi.h"
/*
@@ -24,6 +26,18 @@ typedef struct rank_context
int64 rank; /* current rank */
} rank_context;
+
+typedef struct leadlag_const_context
+{
+ int64 next; /* the index of the lead / lagged value */
+} leadlag_const_context;
+
+/*
+ * lead-lag process helpers
+ */
+#define ISNULL_INDEX(i) (2 * (i))
+#define HAVESCANNED_INDEX(i) ((2 * (i)) + 1)
+
/*
* ntile process information
*/
@@ -280,7 +294,8 @@ window_ntile(PG_FUNCTION_ARGS)
* common operation of lead() and lag()
* For lead() forward is true, whereas for lag() it is false.
* withoffset indicates we have an offset second argument.
- * withdefault indicates we have a default third argument.
+ * withdefault indicates we have a default third argument. We'll only
+ * return this default if the offset we want is outside of the partition.
*/
static Datum
leadlag_common(FunctionCallInfo fcinfo,
@@ -290,8 +305,24 @@ leadlag_common(FunctionCallInfo fcinfo,
int32 offset;
bool const_offset;
Datum result;
- bool isnull;
- bool isout;
+ bool isnull = false;
+ bool isout = false;
+ bool ignore_nulls;
+
+ /**
+ * A ** pointer as we keep a Bitmapset * in the partition context, and
+ * WinGetPartitionLocalMemory returns a pointer to whatever's in the
+ * context.
+ */
+ Bitmapset **null_values;
+
+ /*
+ * We want to set the markpos (the earliest tuple we can access) as
+ * aggressively as possible to save memory, but if the offset isn't
+ * constant we really need random access on the partition (so can't mark
+ * at all).
+ */
+ ignore_nulls = (WinGetFrameOptions(winobj) & FRAMEOPTION_IGNORE_NULLS) != 0;
if (withoffset)
{
@@ -305,21 +336,247 @@ leadlag_common(FunctionCallInfo fcinfo,
offset = 1;
const_offset = true;
}
+ if (!forward)
+ {
+ offset = -offset;
+ }
- result = WinGetFuncArgInPartition(winobj, 0,
- (forward ? offset : -offset),
- WINDOW_SEEK_CURRENT,
- const_offset,
- &isnull, &isout);
+ if (ignore_nulls)
+ {
+ if(const_offset)
+ { int count_forward = 0;
+ leadlag_const_context *context;
+
+ /*
+ * We can process a constant offset much more efficiently; initially
+ * we'll scan through the first <offset> non-null rows, and store that
+ * index. On subsequent rows we'll decide whether to push that index
+ * forwards to the next non-null value, or just return it again.
+ */
+ context = WinGetPartitionLocalMemory(winobj, sizeof(leadlag_const_context));
+
+ /*
+ * Set the forward flag based on the direction of traversal - remember
+ * we can have a LEAD or LAG of -1, and that should be equivalent to a
+ * LAG or LEAD of 1 respectively.
+ */
+ forward = offset == 0 ? forward : (offset > 0);
+
+ if (WinGetCurrentPosition(winobj) == 0)
+ if (forward)
+ count_forward = offset;
+ else
+ context->next = offset; /* LAG, so offset is negative */
+ else
+ {
+ /*
+ * LEADs and LAGs are actually pretty similar - the decision of
+ * whether or not to push our offset value forwards depends on the
+ * current row (for LEADs) or the previous row (for LAGs) is NULL
+ * - hence the (forward ? 0 : -1) below.
+ */
+ result = WinGetFuncArgInPartition(winobj, 0,
+ forward ? 0 : -1,
+ WINDOW_SEEK_CURRENT,
+ forward,
+ &isnull, &isout);
+ if (!isnull)
+ count_forward = 1;
+ }
+
+ /*
+ * Count forward through the rows, skipping nulls and terminating if
+ * we run off the end of the window.
+ */
+ for (; count_forward > 0 && !isout; --count_forward)
+ {
+ do
+ {
+ /*
+ * Conveniently, calling WinGetFuncArgInPartition with an
+ * absolute index less than zero (correctly) sets isout and
+ * isnull to true
+ */
+ result = WinGetFuncArgInPartition(winobj, 0,
+ ++(context->next),
+ WINDOW_SEEK_HEAD,
+ !forward,
+ &isnull, &isout);
+ }
+ while (isnull && !isout);
+ }
+
+ result = WinGetFuncArgInPartition(winobj, 0,
+ context->next,
+ WINDOW_SEEK_HEAD,
+ !forward,
+ &isnull, &isout);
+ }
+ else
+ {
+ int64 scanning,
+ current = WinGetCurrentPosition(winobj);
+ bool scanForward;
+
+ /*
+ * This case is a little complicated; we're defining "IGNORE NULLS" as
+ * "run the query, and pretend the rows with nulls in them don't
+ * exist". This means that we'll scan from the current row an 'offset'
+ * number of non-null rows, and then return that one.
+ *
+ * As the offset isn't constant we need efficient random access to the
+ * partition, as we'll check upto O(partition size) tuples for each
+ * row we're calculating the window function value for.
+ */
+
+ null_values = (Bitmapset **) WinGetPartitionLocalMemory(winobj, sizeof(Bitmapset *));
+
+ if (*null_values == NULL)
+ {
+ MemoryContext oldcxt;
+
+ /*
+ * Accessing tuples is expensive, so we'll keep track of the ones
+ * we've accessed (more specifically, if they're null or not).
+ * We'll need one bit for whether the value is null and one bit
+ * for whether we've checked that tuple or not. We'll keep these
+ * two bits together (as opposed to having two separate bitmaps)
+ * to improve cache locality.
+ *
+ * However, we'd lose the efficient gains if we keep having to
+ * resize the Bitmapset (by setting higher and higher bits). We
+ * know the maximum number of bits we'll ever need, so we'll use
+ * bms_make_singleton to force our Bitmapset up to the required
+ * size.
+ */
+ int64 bits_needed = 2 * WinGetPartitionRowCount(winobj);
+
+ oldcxt = MemoryContextSwitchTo(GetMemoryChunkContext(null_values));
+ *null_values = bms_make_singleton(bits_needed + 1);
+ MemoryContextSwitchTo(oldcxt);
+ }
+
+ /*
+ * We use offset >= 0 instead of just forward as the offset might be
+ * in the opposite direction to the way we're scanning. We'll then
+ * force offset to be positive to make counting down the rows easier.
+ */
+ scanForward = offset == 0 ? forward : (offset > 0);
+ offset = abs(offset);
+
+ for (scanning = current;; scanForward ? ++scanning : --scanning)
+ {
+ if (scanning < 0 || scanning >= WinGetPartitionRowCount(winobj))
+ {
+ isout = true;
+
+ /*
+ * As we're out of the window we want to return NULL or the
+ * default value, but not whatever's left in result. We'll use
+ * the isnull flag to say "ignore it"!
+ */
+ isnull = true;
+ result = (Datum) 0;
+
+ break;
+ }
+
+ if (bms_is_member(HAVESCANNED_INDEX(scanning), *null_values))
+ {
+ isnull = bms_is_member(ISNULL_INDEX(scanning), *null_values);
+ }
+ else
+ {
+ /*
+ * first time we've accessed this index; let's see if it's
+ * null:
+ */
+ result = WinGetFuncArgInPartition(winobj, 0,
+ scanning,
+ WINDOW_SEEK_HEAD,
+ false,
+ &isnull, &isout);
+ if (isout)
+ break;
+
+ bms_add_member(*null_values, HAVESCANNED_INDEX(scanning));
+ if (isnull)
+ {
+ bms_add_member(*null_values, ISNULL_INDEX(scanning));
+ }
+ }
+
+ /*
+ * Now the isnull flag is set correctly. If !isnull there's a
+ * chance that we may stop iterating here:
+ */
+ if (!isnull)
+ {
+ if (offset == 0)
+ {
+ result = WinGetFuncArgInPartition(winobj, 0,
+ scanning,
+ WINDOW_SEEK_HEAD,
+ false,
+ &isnull, &isout);
+ break;
+ }
+ else
+ --offset; /* it's not null, so we're one step closer to
+ * the value we want */
+ }
+ else if (scanning == current)
+ {
+ /*--------
+ * A slight edge case. Consider:
+ *
+ * =================
+ * A | lag(A, 1)
+ * =================
+ * 1 | NULL
+ * 2 | 1
+ * NULL | ?
+ * =================
+ *
+ * Does a lag of one when the current value is null mean go back to the first
+ * non-null value (i.e. 2), or find the previous non-null value of the first
+ * non-null value (i.e. 1)? We're implementing the former semantics, so we'll
+ * need to correct slightly:
+ *--------
+ */
+ --offset;
+ }
+ }
+ }
+ }
+ else
+ {
+ /*
+ * We don't care about nulls; just get the row at the required offset.
+ */
+ result = WinGetFuncArgInPartition(winobj, 0,
+ offset,
+ WINDOW_SEEK_CURRENT,
+ const_offset,
+ &isnull, &isout);
+ }
if (isout)
{
/*
- * target row is out of the partition; supply default value if
- * provided. otherwise it'll stay NULL
+ * Target row is out of the partition; supply default value if
+ * provided.
*/
if (withdefault)
result = WinGetFuncArgCurrent(winobj, 2, &isnull);
+ else
+ {
+ /*
+ * Don't return whatever's lying around in result, force the
+ * output to null if there's no default.
+ */
+ Assert(isnull);
+ }
}
if (isnull)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 18d4991..8b18db4 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -406,19 +406,38 @@ typedef struct SortBy
* For entries in a WINDOW list, "name" is the window name being defined.
* For OVER clauses, we use "name" for the "OVER window" syntax, or "refname"
* for the "OVER (window)" syntax, which is subtly different --- the latter
- * implies overriding the window frame clause.
+ * implies overriding the window frame clause. The semantics of each override
+ * depends on the field.
*/
typedef struct WindowDef
{
NodeTag type;
- char *name; /* window's own name */
- char *refname; /* referenced window name, if any */
- List *partitionClause; /* PARTITION BY expression list */
- List *orderClause; /* ORDER BY (list of SortBy) */
- int frameOptions; /* frame_clause options, see below */
- Node *startOffset; /* expression for starting bound, if any */
- Node *endOffset; /* expression for ending bound, if any */
- int location; /* parse location, or -1 if none/unknown */
+ /* Window's own name. This must be NULL for overrides. */
+ char *name;
+ /* Referenced window name, if any. This must be present on overrides. */
+ char *refname;
+ /*
+ * PARTITION BY expression list. If an override leaves this NULL, the
+ * parent's partitionClause will be used.
+ */
+ List *partitionClause;
+ /*
+ * ORDER BY (list of SortBy). This field is ignored in overrides - the
+ * parent's value will always be used.
+ */
+ List *orderClause;
+ /*
+ * The remaining fields in this struct must be specified on overrides,
+ * even if the override's value is the same as the parent's.
+ */
+ /* frame_clause options, see below */
+ int frameOptions;
+ /* Expression for starting bound, if any */
+ Node *startOffset;
+ /* expression for ending bound, if any */
+ Node *endOffset;
+ /* parse location, or -1 if none/unknown */
+ int location;
} WindowDef;
/*
@@ -443,6 +462,7 @@ typedef struct WindowDef
#define FRAMEOPTION_END_VALUE_PRECEDING 0x00800 /* end is V. P. */
#define FRAMEOPTION_START_VALUE_FOLLOWING 0x01000 /* start is V. F. */
#define FRAMEOPTION_END_VALUE_FOLLOWING 0x02000 /* end is V. F. */
+#define FRAMEOPTION_IGNORE_NULLS 0x04000
#define FRAMEOPTION_START_VALUE \
(FRAMEOPTION_START_VALUE_PRECEDING | FRAMEOPTION_START_VALUE_FOLLOWING)
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 61fae22..c11c65a 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -180,6 +180,7 @@ PG_KEYWORD("hold", HOLD, UNRESERVED_KEYWORD)
PG_KEYWORD("hour", HOUR_P, UNRESERVED_KEYWORD)
PG_KEYWORD("identity", IDENTITY_P, UNRESERVED_KEYWORD)
PG_KEYWORD("if", IF_P, UNRESERVED_KEYWORD)
+PG_KEYWORD("ignore", IGNORE, UNRESERVED_KEYWORD)
PG_KEYWORD("ilike", ILIKE, TYPE_FUNC_NAME_KEYWORD)
PG_KEYWORD("immediate", IMMEDIATE, UNRESERVED_KEYWORD)
PG_KEYWORD("immutable", IMMUTABLE, UNRESERVED_KEYWORD)
@@ -314,6 +315,7 @@ PG_KEYWORD("repeatable", REPEATABLE, UNRESERVED_KEYWORD)
PG_KEYWORD("replace", REPLACE, UNRESERVED_KEYWORD)
PG_KEYWORD("replica", REPLICA, UNRESERVED_KEYWORD)
PG_KEYWORD("reset", RESET, UNRESERVED_KEYWORD)
+PG_KEYWORD("respect", RESPECT, UNRESERVED_KEYWORD)
PG_KEYWORD("restart", RESTART, UNRESERVED_KEYWORD)
PG_KEYWORD("restrict", RESTRICT, UNRESERVED_KEYWORD)
PG_KEYWORD("returning", RETURNING, RESERVED_KEYWORD)
diff --git a/src/include/windowapi.h b/src/include/windowapi.h
index 8557464..1d676e8 100644
--- a/src/include/windowapi.h
+++ b/src/include/windowapi.h
@@ -46,6 +46,8 @@ extern void *WinGetPartitionLocalMemory(WindowObject winobj, Size sz);
extern int64 WinGetCurrentPosition(WindowObject winobj);
extern int64 WinGetPartitionRowCount(WindowObject winobj);
+extern int WinGetFrameOptions(WindowObject winobj);
+
extern void WinSetMarkPosition(WindowObject winobj, int64 markpos);
extern bool WinRowsArePeers(WindowObject winobj, int64 pos1, int64 pos2);
diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out
index c2cc742..6b84dc1 100644
--- a/src/test/regress/expected/window.out
+++ b/src/test/regress/expected/window.out
@@ -1781,3 +1781,216 @@ SELECT i, b, bool_and(b) OVER w, bool_or(b) OVER w
5 | t | t | t
(5 rows)
+-- check we haven't reserved words that might break backwards-compatibility:
+CREATE TABLE reserved (
+ ignore text,
+ respect text,
+ nulls text
+);
+DROP TABLE reserved;
+-- testing ignore nulls functionality
+CREATE TEMPORARY TABLE dogs (
+ name text,
+ breed text,
+ age smallint
+);
+INSERT INTO dogs VALUES
+('K-9', 'robot', NULL),
+('alfred', NULL, 8),
+('bones', 'shar pei', NULL),
+('churchill', 'bulldog', NULL),
+('lassie', NULL, 4),
+('mickey', 'poodle', 7),
+('molly', 'poodle', NULL),
+('rover', 'shar pei', 3);
+-- test view definitions are preserved
+CREATE TEMP VIEW v_dogs AS
+ SELECT
+ name,
+ sum(age) OVER (order by age rows between 1 preceding and 1 following) as sum_rows,
+ lag(age, 1) IGNORE NULLS OVER (ORDER BY name DESC) AS lagged_by_1,
+ lag(age, 2) IGNORE NULLS OVER w AS lagged_by_2
+ FROM dogs
+ WINDOW w as (ORDER BY name ASC);
+SELECT pg_get_viewdef('v_dogs');
+ pg_get_viewdef
+--------------------------------------------------------------------------------------------------
+ SELECT dogs.name, +
+ sum(dogs.age) OVER (ORDER BY dogs.age ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS sum_rows,+
+ lag(dogs.age, 1) IGNORE NULLS OVER (ORDER BY dogs.name DESC) AS lagged_by_1, +
+ lag(dogs.age, 2) IGNORE NULLS OVER w AS lagged_by_2 +
+ FROM dogs +
+ WINDOW w AS (ORDER BY dogs.name);
+(1 row)
+
+-- (1) lags by constant
+SELECT name, lag(age) OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lag
+-----------+-----
+ K-9 |
+ alfred |
+ bones | 8
+ churchill |
+ lassie |
+ mickey | 4
+ molly | 7
+ rover |
+(8 rows)
+
+SELECT name, lag(age) RESPECT NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lag
+-----------+-----
+ K-9 |
+ alfred |
+ bones | 8
+ churchill |
+ lassie |
+ mickey | 4
+ molly | 7
+ rover |
+(8 rows)
+
+SELECT name, lag(age) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lag
+-----------+-----
+ K-9 |
+ alfred |
+ bones | 8
+ churchill | 8
+ lassie | 8
+ mickey | 4
+ molly | 7
+ rover | 7
+(8 rows)
+
+-- (2) leads by constant
+SELECT name, lead(age) OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lead
+-----------+------
+ K-9 | 8
+ alfred |
+ bones |
+ churchill | 4
+ lassie | 7
+ mickey |
+ molly | 3
+ rover |
+(8 rows)
+
+SELECT name, lead(age) RESPECT NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lead
+-----------+------
+ K-9 | 8
+ alfred |
+ bones |
+ churchill | 4
+ lassie | 7
+ mickey |
+ molly | 3
+ rover |
+(8 rows)
+
+SELECT name, lead(age) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lead
+-----------+------
+ K-9 | 8
+ alfred | 4
+ bones | 4
+ churchill | 4
+ lassie | 7
+ mickey | 3
+ molly | 3
+ rover |
+(8 rows)
+
+-- (3) lags by expression
+SELECT name, lag(age * 2) OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lag
+-----------+-----
+ K-9 |
+ alfred |
+ bones | 16
+ churchill |
+ lassie |
+ mickey | 8
+ molly | 14
+ rover |
+(8 rows)
+
+SELECT name, lag(age * 2) RESPECT NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lag
+-----------+-----
+ K-9 |
+ alfred |
+ bones | 16
+ churchill |
+ lassie |
+ mickey | 8
+ molly | 14
+ rover |
+(8 rows)
+
+SELECT name, lag(age * 2) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lag
+-----------+-----
+ K-9 |
+ alfred |
+ bones | 16
+ churchill | 16
+ lassie | 16
+ mickey | 8
+ molly | 14
+ rover | 14
+(8 rows)
+
+-- (4) leads by expression
+SELECT name, lead(age * 2) OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lead
+-----------+------
+ K-9 | 16
+ alfred |
+ bones |
+ churchill | 8
+ lassie | 14
+ mickey |
+ molly | 6
+ rover |
+(8 rows)
+
+SELECT name, lead(age * 2) RESPECT NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lead
+-----------+------
+ K-9 | 16
+ alfred |
+ bones |
+ churchill | 8
+ lassie | 14
+ mickey |
+ molly | 6
+ rover |
+(8 rows)
+
+SELECT name, lead(age * 2) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+ name | lead
+-----------+------
+ K-9 | 16
+ alfred | 8
+ bones | 8
+ churchill | 8
+ lassie | 14
+ mickey | 6
+ molly | 6
+ rover |
+(8 rows)
+
+-- these should be errors as the functionality isn't implemented yet:
+SELECT name, first_value(age) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+ERROR: RESPECT NULLS is only implemented for the lead and lag window functions
+LINE 1: SELECT name, first_value(age) IGNORE NULLS OVER (ORDER BY na...
+ ^
+SELECT name, max(age) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+ERROR: RESPECT NULLS is only implemented for the lead and lag window functions
+LINE 1: SELECT name, max(age) IGNORE NULLS OVER (ORDER BY name) FROM...
+ ^
+DROP TABLE dogs CASCADE;
+NOTICE: drop cascades to view v_dogs
diff --git a/src/test/regress/sql/window.sql b/src/test/regress/sql/window.sql
index 31c98eb..13c0a7b 100644
--- a/src/test/regress/sql/window.sql
+++ b/src/test/regress/sql/window.sql
@@ -621,3 +621,66 @@ SELECT to_char(SUM(n::float8) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND 1 FO
SELECT i, b, bool_and(b) OVER w, bool_or(b) OVER w
FROM (VALUES (1,true), (2,true), (3,false), (4,false), (5,true)) v(i,b)
WINDOW w AS (ORDER BY i ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING);
+
+-- check we haven't reserved words that might break backwards-compatibility:
+CREATE TABLE reserved (
+ ignore text,
+ respect text,
+ nulls text
+);
+DROP TABLE reserved;
+
+-- testing ignore nulls functionality
+
+CREATE TEMPORARY TABLE dogs (
+ name text,
+ breed text,
+ age smallint
+);
+
+INSERT INTO dogs VALUES
+('K-9', 'robot', NULL),
+('alfred', NULL, 8),
+('bones', 'shar pei', NULL),
+('churchill', 'bulldog', NULL),
+('lassie', NULL, 4),
+('mickey', 'poodle', 7),
+('molly', 'poodle', NULL),
+('rover', 'shar pei', 3);
+
+-- test view definitions are preserved
+CREATE TEMP VIEW v_dogs AS
+ SELECT
+ name,
+ sum(age) OVER (order by age rows between 1 preceding and 1 following) as sum_rows,
+ lag(age, 1) IGNORE NULLS OVER (ORDER BY name DESC) AS lagged_by_1,
+ lag(age, 2) IGNORE NULLS OVER w AS lagged_by_2
+ FROM dogs
+ WINDOW w as (ORDER BY name ASC);
+SELECT pg_get_viewdef('v_dogs');
+
+-- (1) lags by constant
+SELECT name, lag(age) OVER (ORDER BY name) FROM dogs ORDER BY name;
+SELECT name, lag(age) RESPECT NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+SELECT name, lag(age) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+
+-- (2) leads by constant
+SELECT name, lead(age) OVER (ORDER BY name) FROM dogs ORDER BY name;
+SELECT name, lead(age) RESPECT NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+SELECT name, lead(age) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+
+-- (3) lags by expression
+SELECT name, lag(age * 2) OVER (ORDER BY name) FROM dogs ORDER BY name;
+SELECT name, lag(age * 2) RESPECT NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+SELECT name, lag(age * 2) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+
+-- (4) leads by expression
+SELECT name, lead(age * 2) OVER (ORDER BY name) FROM dogs ORDER BY name;
+SELECT name, lead(age * 2) RESPECT NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+SELECT name, lead(age * 2) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+
+-- these should be errors as the functionality isn't implemented yet:
+SELECT name, first_value(age) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+SELECT name, max(age) IGNORE NULLS OVER (ORDER BY name) FROM dogs ORDER BY name;
+
+DROP TABLE dogs CASCADE;
\ No newline at end of file
--
Sent via pgsql-hackers mailing list ([email protected])
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers