Repository: timescale/timescaledb (2026-06-06T12:12:44Z)

Issue:
XX000 attribute number exceeds number of columns in COPY WHERE
```
CREATE TABLE sensor_readings (  
    recorded_at  timestamptz NOT NULL,  
    dropped_col  int,  
    sensor_name  text,   
    temperature  int
);       
SELECT create_hypertable('sensor_readings', 'recorded_at');  
ALTER TABLE sensor_readings DROP COLUMN dropped_col;

COPY sensor_readings (recorded_at, sensor_name, temperature)  
FROM STDIN WHERE temperature > 10;
2024-01-01    office  20                   
\.  
```


Discussion:

Patch:
diff --git a/.unreleased/pr_9983 b/.unreleased/pr_9983
new file mode 100644
index 00000000000..f26af024d2f
--- /dev/null
+++ b/.unreleased/pr_9983
@@ -0,0 +1,1 @@
+Fixes: #9977 Fix COPY WHERE into a hypertable with dropped columns
diff --git a/src/copy.c b/src/copy.c
index 75e76b1c1d5..b4654f3c67e 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -1098,6 +1098,19 @@ copyfrom(CopyChunkState *ccstate, ParseState *pstate, Hypertable *ht, MemoryCont
 
 		ExecStoreVirtualTuple(myslot);
 
+		/*
+		 * Apply the WHERE clause before the tuple is converted to the chunk
+		 * layout. The chunk can have different physical layout.
+		 */
+		if (qualexpr != NULL)
+		{
+			econtext->ecxt_scantuple = myslot;
+			if (!ExecQual(qualexpr, econtext))
+			{
+				continue;
+			}
+		}
+
 		/* Calculate the tuple's point in the N-dimensional hyperspace */
 		point = ts_hyperspace_calculate_point(ht->space, myslot);
 
@@ -1190,15 +1203,6 @@ copyfrom(CopyChunkState *ccstate, ParseState *pstate, Hypertable *ht, MemoryCont
 			}
 		}
 
-		if (qualexpr != NULL)
-		{
-			econtext->ecxt_scantuple = myslot;
-			if (!ExecQual(qualexpr, econtext))
-			{
-				continue;
-			}
-		}
-
 		/*
 		 * Set the result relation in the executor state to the target chunk.
 		 * This makes sure that the tuple gets inserted into the correct


Test patch:
diff --git a/test/expected/copy.out b/test/expected/copy.out
index 653f40db7de..7d8413067d4 100644
--- a/test/expected/copy.out
+++ b/test/expected/copy.out
@@ -678,6 +678,41 @@ SELECT * FROM table_with_layout_change ORDER BY time, value7;
     5 |    724
     6 |    725
 
+-- COPY WHERE into a hypertable whose chunks have dropped columns
+COPY table_with_layout_change (value7, time) FROM STDIN DELIMITER ',' NULL AS 'null' WHERE value7 > 800;
+SELECT * FROM table_with_layout_change ORDER BY time, value7;
+ time | value7 
+------+--------
+    1 |    700
+    1 |    700
+    1 |    700
+    1 |    700
+    1 |    700
+    1 |    700
+    1 |    722
+    2 |    700
+    2 |    700
+    2 |    700
+    2 |    700
+    2 |    700
+    2 |    700
+    2 |    700
+    2 |    700
+    2 |    721
+    2 |    850
+    3 |    700
+    3 |    700
+    3 |    700
+    3 |    700
+    3 |    723
+    3 |    860
+    4 |    700
+    4 |    700
+    4 |    726
+    5 |    700
+    5 |    724
+    6 |    725
+
 -- verify check constraints work
 CREATE TABLE test_check(a INT, b TIMESTAMPTZ);
 ALTER TABLE test_check ADD CONSTRAINT c1 CHECK (a > 7);
diff --git a/test/sql/copy.sql b/test/sql/copy.sql
index 6ea2d11a110..74dba1400ef 100644
--- a/test/sql/copy.sql
+++ b/test/sql/copy.sql
@@ -545,6 +545,15 @@ COPY table_with_layout_change (value7, time) FROM STDIN DELIMITER ',' NULL AS 'n
 
 SELECT * FROM table_with_layout_change ORDER BY time, value7;
 
+-- COPY WHERE into a hypertable whose chunks have dropped columns
+COPY table_with_layout_change (value7, time) FROM STDIN DELIMITER ',' NULL AS 'null' WHERE value7 > 800;
+850,2
+727,1
+860,3
+\.
+
+SELECT * FROM table_with_layout_change ORDER BY time, value7;
+
 -- verify check constraints work
 CREATE TABLE test_check(a INT, b TIMESTAMPTZ);
 ALTER TABLE test_check ADD CONSTRAINT c1 CHECK (a > 7);