From 27f07926994b14e14049bd56c9e0e2eac177f1a1 Mon Sep 17 00:00:00 2001 From: qsliu Date: Thu, 16 Jul 2026 19:51:18 +0800 Subject: [PATCH] fix: analyze a copy of the view body in CREATE VIEW hook The pg_ducklake utility hook parse-analyzes every CREATE VIEW body to detect duckdb_row target columns (RewriteDuckdbRowViewStmt). It fed the ViewStmt's raw query directly to parse_analyze, but parse analysis rewrites the raw tree in place: transformFromClauseItem replaces a JoinExpr's larg/rarg with RangeTblRef nodes, and transformSubLink replaces SubLink->subselect (a raw SelectStmt) with an analyzed Query. For any view we do not end up rewriting (the common case), the hook returned without using the analyzed result, leaving stmt->query corrupted. DefineView then re-ran parse analysis on the mutated tree and failed: - #229: explicit JOIN/LEFT JOIN -> "unrecognized node type: 63" (transformFromClauseItem sees a RangeTblRef where a raw FROM item is expected). - #230: EXISTS / IN (SELECT) / correlated subquery -> "unexpected non-SELECT command in SubLink" (transformSubLink sees subselect already a Query). CTE views could crash the backend outright. Both issues share this single root cause. Fix: parse-analyze a copyObject of stmt->query so the original raw tree stays pristine for DefineView. Plain SELECT and comma-join views worked before only because those shapes are not mutated in place. Adds create_view_shapes regression test: JOIN, LEFT JOIN, EXISTS, correlated scalar subquery, IN (SELECT), CTE, UNION, and LATERAL views over heap tables, plus JOIN and EXISTS over ducklake tables, each verifying SELECT returns correct rows. Co-Authored-By: Claude Fable 5 --- pg_ducklake/src/hooks.cpp | 5 +- .../expected/create_view_shapes.out | 134 ++++++++++++++++++ pg_ducklake/test/regression/schedule | 1 + .../regression/sql/create_view_shapes.sql | 78 ++++++++++ 4 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 pg_ducklake/test/regression/expected/create_view_shapes.out create mode 100644 pg_ducklake/test/regression/sql/create_view_shapes.sql diff --git a/pg_ducklake/src/hooks.cpp b/pg_ducklake/src/hooks.cpp index 82137ec5..0749032b 100644 --- a/pg_ducklake/src/hooks.cpp +++ b/pg_ducklake/src/hooks.cpp @@ -447,8 +447,11 @@ RewriteDuckdbRowViewStmt(ViewStmt *stmt, PlannedStmt *pstmt, const char *query_s if (!OidIsValid(duckdb_row_oid)) return; + /* Analyze a copy: parse analysis rewrites the raw tree in place (JoinExpr + * larg/rarg, SubLink->subselect), which would corrupt stmt->query for + * DefineView's own analysis when we don't rewrite the view ourselves. */ RawStmt *rawstmt = makeNode(RawStmt); - rawstmt->stmt = stmt->query; + rawstmt->stmt = (Node *)copyObjectImpl(stmt->query); rawstmt->stmt_location = pstmt->stmt_location; rawstmt->stmt_len = pstmt->stmt_len; #if PG_VERSION_NUM >= 150000 diff --git a/pg_ducklake/test/regression/expected/create_view_shapes.out b/pg_ducklake/test/regression/expected/create_view_shapes.out new file mode 100644 index 00000000..fcd8aeed --- /dev/null +++ b/pg_ducklake/test/regression/expected/create_view_shapes.out @@ -0,0 +1,134 @@ +-- CREATE VIEW over query shapes that carry raw JoinExpr / SubLink nodes. +-- The utility hook parse-analyzes the view body to detect duckdb_row columns; +-- doing that on the original raw tree corrupted it (JoinExpr larg/rarg and +-- SubLink->subselect are rewritten in place), so DefineView's own parse +-- analysis then failed. These must all succeed and return correct rows. +-- Heap tables -------------------------------------------------------------- +CREATE TABLE h1 (id int PRIMARY KEY, val int); +CREATE TABLE h2 (id int PRIMARY KEY, label text); +INSERT INTO h1 VALUES (1, 10), (2, 20), (3, 30); +INSERT INTO h2 VALUES (1, 'a'), (2, 'b'); +-- Explicit INNER JOIN +CREATE VIEW hv_join AS + SELECT h1.id, h2.label FROM h1 JOIN h2 ON h2.id = h1.id; +SELECT * FROM hv_join ORDER BY id; + id | label +----+------- + 1 | a + 2 | b +(2 rows) + +-- Explicit LEFT JOIN +CREATE VIEW hv_left_join AS + SELECT h1.id, h2.label FROM h1 LEFT JOIN h2 ON h2.id = h1.id; +SELECT * FROM hv_left_join ORDER BY id; + id | label +----+------- + 1 | a + 2 | b + 3 | +(3 rows) + +-- EXISTS sublink in WHERE +CREATE VIEW hv_exists AS + SELECT h1.id FROM h1 WHERE EXISTS (SELECT 1 FROM h2 WHERE h2.id = h1.id); +SELECT * FROM hv_exists ORDER BY id; + id +---- + 1 + 2 +(2 rows) + +-- Correlated scalar subquery in the target list +CREATE VIEW hv_scalar_sub AS + SELECT h1.id, (SELECT h2.label FROM h2 WHERE h2.id = h1.id) AS label FROM h1; +SELECT * FROM hv_scalar_sub ORDER BY id; + id | label +----+------- + 1 | a + 2 | b + 3 | +(3 rows) + +-- IN (SELECT ...) sublink +CREATE VIEW hv_in_sub AS + SELECT h1.id FROM h1 WHERE h1.id IN (SELECT h2.id FROM h2); +SELECT * FROM hv_in_sub ORDER BY id; + id +---- + 1 + 2 +(2 rows) + +-- CTE +CREATE VIEW hv_cte AS + WITH c AS (SELECT id, label FROM h2) + SELECT h1.id, c.label FROM h1 JOIN c ON c.id = h1.id; +SELECT * FROM hv_cte ORDER BY id; + id | label +----+------- + 1 | a + 2 | b +(2 rows) + +-- UNION set operation +CREATE VIEW hv_union AS + SELECT id FROM h1 UNION SELECT id FROM h2; +SELECT * FROM hv_union ORDER BY id; + id +---- + 1 + 2 + 3 +(3 rows) + +-- LATERAL join +CREATE VIEW hv_lateral AS + SELECT h1.id, sub.label + FROM h1 LEFT JOIN LATERAL (SELECT h2.label FROM h2 WHERE h2.id = h1.id) sub ON true; +SELECT * FROM hv_lateral ORDER BY id; + id | label +----+------- + 1 | a + 2 | b + 3 | +(3 rows) + +-- CREATE OR REPLACE VIEW over a JOIN +CREATE OR REPLACE VIEW hv_join AS + SELECT h1.id, h2.label, h1.val FROM h1 JOIN h2 ON h2.id = h1.id; +SELECT * FROM hv_join ORDER BY id; + id | label | val +----+-------+----- + 1 | a | 10 + 2 | b | 20 +(2 rows) + +-- DuckLake tables ---------------------------------------------------------- +CREATE TABLE d1 (id int, val int) USING ducklake; +CREATE TABLE d2 (id int, label text) USING ducklake; +INSERT INTO d1 VALUES (1, 10), (2, 20), (3, 30); +INSERT INTO d2 VALUES (1, 'a'), (2, 'b'); +CREATE VIEW dv_join AS + SELECT d1.id, d2.label FROM d1 JOIN d2 ON d2.id = d1.id; +SELECT * FROM dv_join ORDER BY id; + id | label +----+------- + 1 | a + 2 | b +(2 rows) + +CREATE VIEW dv_exists AS + SELECT d1.id FROM d1 WHERE EXISTS (SELECT 1 FROM d2 WHERE d2.id = d1.id); +SELECT * FROM dv_exists ORDER BY id; + id +---- + 1 + 2 +(2 rows) + +-- Cleanup +DROP VIEW hv_join, hv_left_join, hv_exists, hv_scalar_sub, hv_in_sub, + hv_cte, hv_union, hv_lateral, dv_join, dv_exists; +DROP TABLE h1, h2; +DROP TABLE d1, d2; diff --git a/pg_ducklake/test/regression/schedule b/pg_ducklake/test/regression/schedule index ebce579b..7333aad6 100644 --- a/pg_ducklake/test/regression/schedule +++ b/pg_ducklake/test/regression/schedule @@ -49,3 +49,4 @@ test: import_foreign_schema test: secrets test: access_control test: quoted_names +test: create_view_shapes diff --git a/pg_ducklake/test/regression/sql/create_view_shapes.sql b/pg_ducklake/test/regression/sql/create_view_shapes.sql new file mode 100644 index 00000000..14679fb9 --- /dev/null +++ b/pg_ducklake/test/regression/sql/create_view_shapes.sql @@ -0,0 +1,78 @@ +-- CREATE VIEW over query shapes that carry raw JoinExpr / SubLink nodes. +-- The utility hook parse-analyzes the view body to detect duckdb_row columns; +-- doing that on the original raw tree corrupted it (JoinExpr larg/rarg and +-- SubLink->subselect are rewritten in place), so DefineView's own parse +-- analysis then failed. These must all succeed and return correct rows. + +-- Heap tables -------------------------------------------------------------- +CREATE TABLE h1 (id int PRIMARY KEY, val int); +CREATE TABLE h2 (id int PRIMARY KEY, label text); +INSERT INTO h1 VALUES (1, 10), (2, 20), (3, 30); +INSERT INTO h2 VALUES (1, 'a'), (2, 'b'); + +-- Explicit INNER JOIN +CREATE VIEW hv_join AS + SELECT h1.id, h2.label FROM h1 JOIN h2 ON h2.id = h1.id; +SELECT * FROM hv_join ORDER BY id; + +-- Explicit LEFT JOIN +CREATE VIEW hv_left_join AS + SELECT h1.id, h2.label FROM h1 LEFT JOIN h2 ON h2.id = h1.id; +SELECT * FROM hv_left_join ORDER BY id; + +-- EXISTS sublink in WHERE +CREATE VIEW hv_exists AS + SELECT h1.id FROM h1 WHERE EXISTS (SELECT 1 FROM h2 WHERE h2.id = h1.id); +SELECT * FROM hv_exists ORDER BY id; + +-- Correlated scalar subquery in the target list +CREATE VIEW hv_scalar_sub AS + SELECT h1.id, (SELECT h2.label FROM h2 WHERE h2.id = h1.id) AS label FROM h1; +SELECT * FROM hv_scalar_sub ORDER BY id; + +-- IN (SELECT ...) sublink +CREATE VIEW hv_in_sub AS + SELECT h1.id FROM h1 WHERE h1.id IN (SELECT h2.id FROM h2); +SELECT * FROM hv_in_sub ORDER BY id; + +-- CTE +CREATE VIEW hv_cte AS + WITH c AS (SELECT id, label FROM h2) + SELECT h1.id, c.label FROM h1 JOIN c ON c.id = h1.id; +SELECT * FROM hv_cte ORDER BY id; + +-- UNION set operation +CREATE VIEW hv_union AS + SELECT id FROM h1 UNION SELECT id FROM h2; +SELECT * FROM hv_union ORDER BY id; + +-- LATERAL join +CREATE VIEW hv_lateral AS + SELECT h1.id, sub.label + FROM h1 LEFT JOIN LATERAL (SELECT h2.label FROM h2 WHERE h2.id = h1.id) sub ON true; +SELECT * FROM hv_lateral ORDER BY id; + +-- CREATE OR REPLACE VIEW over a JOIN +CREATE OR REPLACE VIEW hv_join AS + SELECT h1.id, h2.label, h1.val FROM h1 JOIN h2 ON h2.id = h1.id; +SELECT * FROM hv_join ORDER BY id; + +-- DuckLake tables ---------------------------------------------------------- +CREATE TABLE d1 (id int, val int) USING ducklake; +CREATE TABLE d2 (id int, label text) USING ducklake; +INSERT INTO d1 VALUES (1, 10), (2, 20), (3, 30); +INSERT INTO d2 VALUES (1, 'a'), (2, 'b'); + +CREATE VIEW dv_join AS + SELECT d1.id, d2.label FROM d1 JOIN d2 ON d2.id = d1.id; +SELECT * FROM dv_join ORDER BY id; + +CREATE VIEW dv_exists AS + SELECT d1.id FROM d1 WHERE EXISTS (SELECT 1 FROM d2 WHERE d2.id = d1.id); +SELECT * FROM dv_exists ORDER BY id; + +-- Cleanup +DROP VIEW hv_join, hv_left_join, hv_exists, hv_scalar_sub, hv_in_sub, + hv_cte, hv_union, hv_lateral, dv_join, dv_exists; +DROP TABLE h1, h2; +DROP TABLE d1, d2;