Repository: duckdb/duckdb (2026-07-02T10:47:49Z)

Issue:
Inherited named window fails to resolve earlier base window
### What happens?

On current DuckDB `main`(0361de4), it rejects a query that defines a named window in terms of an earlier named window in the same `WINDOW` clause.

The issue does not reproduce on DuckDB `v1.5.4`.


### To Reproduce

On current DuckDB `main`(0361de4),

```sql
SELECT count(*) OVER w AS x
FROM (VALUES (1), (2)) AS t(a)
WINDOW
  base AS (ORDER BY a),
  w AS (base ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
```

Run:

```sh
./build/release/duckdb -json -f bug.sql
```

On main, this produces:

```text
Parser Error: window "base" does not exist
```

## Expected Behavior

The query should succeed. On DuckDB v1.5.4, it returns:

```json
[{"x":2},{"x":1}]
```

The equivalent inline window specification also succeeds on main:

```sql
SELECT count(*) OVER (
  ORDER BY a
  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
) AS x
FROM (VALUES (1), (2)) AS t(a);
```

which returns:

```json
[{"x":2},{"x":1}]
```

This appears to be specific to inheriting from a previously defined named window inside the same `WINDOW` clause. The failure is not specific to `ROWS`; these variants also fail on main:

```sql
SELECT count(*) OVER w AS x
FROM (VALUES (1), (2)) AS t(a)
WINDOW
  base AS (ORDER BY a),
  w AS (base);
```

```sql
SELECT count(*) OVER w AS x
FROM (VALUES (1), (2)) AS t(a)
WINDOW
  base AS (ORDER BY a),
  w AS (base RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
```

```sql
SELECT count(*) OVER w AS x
FROM (VALUES (1), (2)) AS t(a)
WINDOW
  base AS (ORDER BY a),
  w AS (base GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
```

I think this may be related to the PEG transformer registering named windows after transforming the whole `WINDOW` list. `TransformWindowFrameNameContents` looks up the base window through `GetWindowClause`, but the earlier named window has not yet been inserted into `window_clauses` when a later window definition in the same list inherits from it.


### OS:

macOS 15.7.4, arm64

### DuckDB Version:

main, commit 0361de4

### DuckDB Client:

CLI

### Hardware:

_No response_

### Full Name:

Yibo Dong

### Affiliation:

National University of Singapore

### Did you include all relevant configuration (e.g., CPU architecture, Linux distribution) to reproduce the issue?

- [x] Yes, I have

### Did you include all code required to reproduce the issue?

- [x] Yes, I have

### Did you include all relevant data sets for reproducing the issue?

Yes


Discussion:
Opened #23569 for this — the PEG parser only registered the named windows after transforming the whole WINDOW clause, so `w AS (base ...)` couldn't see `base` yet. The fix registers each window as it's transformed; forward and self references still error as before.
Happy to address any issue.
@vismaytiwari Thanks for checking this!



Patch:
diff --git a/src/parser/peg/transformer/transform_select.cpp b/src/parser/peg/transformer/transform_select.cpp
index bf6a51f34440..211776fe9cd7 100644
--- a/src/parser/peg/transformer/transform_select.cpp
+++ b/src/parser/peg/transformer/transform_select.cpp
@@ -247,19 +247,7 @@ unique_ptr<SelectStatement> PEGTransformerFactory::TransformSimpleSelect(PEGTran
 	auto &list_pr = parse_result.Cast<ListParseResult>();
 	auto &opt_window_clause = list_pr.Child<OptionalParseResult>(4);
 	if (opt_window_clause.HasResult()) {
-		auto window_functions =
-		    transformer.Transform<vector<unique_ptr<ParsedExpression>>>(opt_window_clause.GetResult());
-		for (auto &window_func : window_functions) {
-			D_ASSERT(!window_func->GetAlias().empty());
-			auto window_name = window_func->GetAlias();
-			window_func->ClearAlias();
-			auto it = transformer.window_clauses.find(window_name);
-			if (it != transformer.window_clauses.end()) {
-				throw ParserException("window \"%s\" is already defined", window_name.GetIdentifierName());
-			}
-			auto window_function = unique_ptr_cast<ParsedExpression, WindowExpression>(std::move(window_func));
-			transformer.window_clauses[window_name] = std::move(window_function);
-		}
+		transformer.Transform<vector<unique_ptr<ParsedExpression>>>(opt_window_clause.GetResult());
 	}
 	auto select_node = transformer.Transform<unique_ptr<SelectNode>>(list_pr.Child<ListParseResult>(0));
 	transformer.TransformOptional<unique_ptr<ParsedExpression>>(list_pr, 1, select_node->where_clause);
@@ -282,18 +270,14 @@ unique_ptr<SelectStatement> PEGTransformerFactory::TransformSimpleSelect(PEGTran
 	return select_statement;
 }
 
-static void RegisterWindowClauses(PEGTransformer &transformer, vector<unique_ptr<ParsedExpression>> window_functions) {
-	for (auto &window_func : window_functions) {
-		D_ASSERT(!window_func->GetAlias().empty());
-		auto window_name = window_func->GetAlias();
-		window_func->ClearAlias();
-		auto it = transformer.window_clauses.find(window_name);
-		if (it != transformer.window_clauses.end()) {
-			throw ParserException("window \"%s\" is already defined", window_name.GetIdentifierName());
-		}
-		auto window_function = unique_ptr_cast<ParsedExpression, WindowExpression>(std::move(window_func));
-		transformer.window_clauses[window_name] = std::move(window_function);
+static void RegisterWindowClause(PEGTransformer &transformer, const Identifier &window_name,
+                                 WindowExpression &window_function) {
+	auto it = transformer.window_clauses.find(window_name);
+	if (it != transformer.window_clauses.end()) {
+		throw ParserException("window \"%s\" is already defined", window_name.GetIdentifierName());
 	}
+	transformer.window_clauses[window_name] =
+	    unique_ptr_cast<ParsedExpression, WindowExpression>(window_function.Copy());
 }
 
 static void PushSimpleSelectRemainder(TransformStack &stack, TransformStackFrame &frame) {
@@ -346,8 +330,7 @@ unique_ptr<TransformResultValue> PEGTransformerFactory::FinalizeSimpleSelectTram
                                                                                        TransformStack &stack,
                                                                                        TransformStackFrame &frame) {
 	if (frame.manual_state == 0) {
-		auto window_functions = frame.TakeResult<vector<unique_ptr<ParsedExpression>>>(4);
-		RegisterWindowClauses(transformer, std::move(window_functions));
+		frame.TakeResult<vector<unique_ptr<ParsedExpression>>>(4);
 		frame.manual_state = 1;
 		PushSimpleSelectRemainder(stack, frame);
 		return nullptr;
@@ -1214,7 +1197,8 @@ unique_ptr<ParsedExpression> PEGTransformerFactory::TransformWindowDefinition(PE
 	transformer.in_window_definition = true;
 	auto window_function = transformer.Transform<unique_ptr<WindowExpression>>(list_pr.Child<ListParseResult>(2));
 	transformer.in_window_definition = false;
-	window_function->SetAlias(list_pr.Child<IdentifierParseResult>(0).identifier);
+	auto window_name = list_pr.Child<IdentifierParseResult>(0).identifier;
+	RegisterWindowClause(transformer, window_name, *window_function);
 	return std::move(window_function);
 }
 
@@ -1233,7 +1217,8 @@ unique_ptr<TransformResultValue> PEGTransformerFactory::FinalizeWindowDefinition
 	auto &list_pr = frame.parse_result.Cast<ListParseResult>();
 	auto window_function = frame.TakeResult<unique_ptr<WindowExpression>>(0);
 	transformer.in_window_definition = false;
-	window_function->SetAlias(list_pr.Child<IdentifierParseResult>(0).identifier);
+	auto window_name = list_pr.Child<IdentifierParseResult>(0).identifier;
+	RegisterWindowClause(transformer, window_name, *window_function);
 	unique_ptr<ParsedExpression> result = std::move(window_function);
 	return make_uniq<TypedTransformResult<unique_ptr<ParsedExpression>>>(std::move(result));
 }


Test patch:
diff --git a/test/sql/window/test_window_clause.test b/test/sql/window/test_window_clause.test
index 39580746098a..4a6ed3344471 100644
--- a/test/sql/window/test_window_clause.test
+++ b/test/sql/window/test_window_clause.test
@@ -79,3 +79,54 @@ statement ok
 SELECT sum(i) over cumulativeSum 
 FROM integers 
 WINDOW cumulativeSum AS ();
+
+# A named window can inherit from an earlier named window in the same WINDOW clause
+query I
+SELECT count(*) OVER w AS x
+FROM (VALUES (1), (2)) AS t(a)
+WINDOW base AS (ORDER BY a), w AS (base ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ORDER BY a;
+----
+2
+1
+
+query I
+SELECT count(*) OVER w AS x
+FROM (VALUES (1), (2)) AS t(a)
+WINDOW base AS (ORDER BY a), w AS (base)
+ORDER BY a;
+----
+1
+2
+
+query I
+SELECT count(*) OVER w AS x
+FROM (VALUES (1), (2)) AS t(a)
+WINDOW base AS (ORDER BY a), w AS (base RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ORDER BY a;
+----
+2
+1
+
+query I
+SELECT count(*) OVER w AS x
+FROM (VALUES (1), (2)) AS t(a)
+WINDOW base AS (ORDER BY a), w AS (base GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ORDER BY a;
+----
+2
+1
+
+statement error
+SELECT count(*) OVER w
+FROM (VALUES (1), (2)) AS t(a)
+WINDOW w AS (base), base AS (ORDER BY a);
+----
+does not exist
+
+statement error
+SELECT count(*) OVER w
+FROM (VALUES (1), (2)) AS t(a)
+WINDOW w AS (w);
+----
+does not exist