Repository: duckdb/duckdb (2026-07-27T08:04:15Z)

Issue:
Query gets incorrect result after NULL related table update
### What happens?

See repro for details.

### To Reproduce

```sql
# name: test/sql/storage/wal/wal_null_update_row_group_pruning.test
# description: Test row-group pruning after WAL replay of NULL updates
# group: [wal]

require skip_reload

load {TEST_DIR}/wal_null_update_row_group_pruning.db

statement ok
PRAGMA disable_checkpoint_on_shutdown

statement ok
PRAGMA wal_autocheckpoint='1TB';

statement ok
CREATE TABLE test AS SELECT i::BIGINT AS a FROM range(2048) tbl(i);

statement ok
CREATE TABLE distinct_test AS SELECT 1::BIGINT AS a FROM range(2048);

statement ok
CHECKPOINT;

statement ok
UPDATE test SET a = NULL WHERE a = 3;

statement ok
UPDATE distinct_test SET a = NULL WHERE rowid = 3;

query I
SELECT count(*) FROM test WHERE a IS NULL;
----
1

query I
SELECT count(*) FROM distinct_test WHERE a IS DISTINCT FROM 1;
----
1

restart

query I
SELECT count(*) FROM test WHERE a IS NULL;
----
1

query I
SELECT count(*) FROM test WHERE CASE WHEN a IS NULL THEN true ELSE false END;
----
1

query I
SELECT count(*) FROM distinct_test WHERE a IS DISTINCT FROM 1;
----
1

query I
SELECT count(*) FROM distinct_test WHERE CASE WHEN a IS DISTINCT FROM 1 THEN true ELSE false END;
----
1
```
gets error message
```sh
[~/Desktop/duckdb] (main)
hjiang@hjiangs-MacBook-Pro$ ./build/reldebug/test/unittest test/sql/storage/wal/wal_null_update_row_group_pruning.test
Filters: test/sql/storage/wal/wal_null_update_row_group_pruning.test
[0/1] (0%): test/sql/storage/wal/wal_null_update_row_group_pruning.test
1. test/sql/storage/wal/wal_null_update_row_group_pruning.test:32
================================================================================
Wrong result in query! (test/sql/storage/wal/wal_null_update_row_group_pruning.test:32)!
================================================================================
SELECT count(*) FROM test WHERE a IS NULL;
================================================================================
Mismatch on row 1, column count_star()(index 1)
0 <> 1
================================================================================
Expected result:
================================================================================
1

================================================================================
Actual result:
================================================================================
0
```

### OS:

macos

### DuckDB Version:

7316202df1f856f339e2442a05dfdada231b1fec

### DuckDB Client:

SQL

### Hardware:

_No response_

### Full Name:

dentiny

### Affiliation:

N/A

### 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:

Patch:
diff --git a/src/storage/table/column_data.cpp b/src/storage/table/column_data.cpp
index be104c6e2978..5a1c278610ae 100644
--- a/src/storage/table/column_data.cpp
+++ b/src/storage/table/column_data.cpp
@@ -445,12 +445,11 @@ FilterPropagateResult ColumnData::CheckZonemap(ColumnScanState &state, TableFilt
 			return FilterPropagateResult::NO_PRUNING_POSSIBLE;
 		}
 	}
-	lock_guard<mutex> l(update_lock);
-	if (!updates) {
+	auto update_stats = GetUpdateStatistics();
+	if (!update_stats) {
 		// no updates - return original result
 		return prune_result;
 	}
-	auto update_stats = updates->GetStatistics();
 	// combine the update and original prune result
 	auto context = state.context.GetClientContext();
 	FilterPropagateResult update_result =
diff --git a/src/storage/table/standard_column_data.cpp b/src/storage/table/standard_column_data.cpp
index 569c38049cd1..839d2e6b9b31 100644
--- a/src/storage/table/standard_column_data.cpp
+++ b/src/storage/table/standard_column_data.cpp
@@ -186,7 +186,11 @@ void StandardColumnData::UpdateColumn(TransactionData transaction, DuckTableEntr
 }
 
 unique_ptr<BaseStatistics> StandardColumnData::GetUpdateStatistics() {
-	auto stats = updates ? updates->GetStatistics() : nullptr;
+	unique_ptr<BaseStatistics> stats;
+	{
+		lock_guard<mutex> update_guard(update_lock);
+		stats = updates ? updates->GetStatistics() : nullptr;
+	}
 	auto validity_stats = validity->GetUpdateStatistics();
 	if (!stats && !validity_stats) {
 		return nullptr;


Test patch:
diff --git a/test/sql/storage/wal/wal_null_update_row_group_pruning.test b/test/sql/storage/wal/wal_null_update_row_group_pruning.test
new file mode 100644
index 000000000000..a70a40bee2d5
--- /dev/null
+++ b/test/sql/storage/wal/wal_null_update_row_group_pruning.test
@@ -0,0 +1,60 @@
+# name: test/sql/storage/wal/wal_null_update_row_group_pruning.test
+# description: Test row-group pruning after WAL replay of NULL updates
+# group: [wal]
+
+require skip_reload
+
+load {TEST_DIR}/wal_null_update_row_group_pruning.db
+
+statement ok
+PRAGMA disable_checkpoint_on_shutdown
+
+statement ok
+PRAGMA wal_autocheckpoint='1TB';
+
+statement ok
+CREATE TABLE test AS SELECT i::BIGINT AS a FROM range(2048) tbl(i);
+
+statement ok
+CREATE TABLE distinct_test AS SELECT 1::BIGINT AS a FROM range(2048);
+
+statement ok
+CHECKPOINT;
+
+statement ok
+UPDATE test SET a = NULL WHERE a = 3;
+
+statement ok
+UPDATE distinct_test SET a = NULL WHERE rowid = 3;
+
+query I
+SELECT count(*) FROM test WHERE a IS NULL;
+----
+1
+
+query I
+SELECT count(*) FROM distinct_test WHERE a IS DISTINCT FROM 1;
+----
+1
+
+restart
+
+query I
+SELECT count(*) FROM test WHERE a IS NULL;
+----
+1
+
+query I
+SELECT count(*) FROM test WHERE CASE WHEN a IS NULL THEN true ELSE false END;
+----
+1
+
+query I
+SELECT count(*) FROM distinct_test WHERE a IS DISTINCT FROM 1;
+----
+1
+
+query I
+SELECT count(*) FROM distinct_test WHERE CASE WHEN a IS DISTINCT FROM 1 THEN true ELSE false END;
+----
+1