Hi,

When a file_fdw table is configured with on_error 'ignore' and a reject_limit option, SELECT queries properly abort with an ERROR once the number of bad rows exceeds the limit. However, ANALYZE silently ignores reject_limit, skips all malformed rows, and finishes with a NOTICE.

Reproduce with this:

printf 'x\ny\n' > /tmp/fdw.csv

CREATE EXTENSION file_fdw;
CREATE SERVER fs FOREIGN DATA WRAPPER file_fdw;
CREATE FOREIGN TABLE ft_bad (a int) SERVER fs
 OPTIONS (filename '/tmp/fdw.csv', format 'csv',
        on_error 'ignore', reject_limit '1');

SELECT * FROM ft_bad;
ERROR:  skipped more than REJECT_LIMIT (1) rows due to data type incompatibility
CONTEXT:  COPY ft_bad, line 2, column a: "y"

ANALYZE ft_bad;
NOTICE:  2 rows were skipped due to data type incompatibility

The fix is straightforward, we just add the same cstate->opts.reject_limit check to
file_acquire_sample_rows(), just like we do for fileIterateForeignScan().

I thought about whether this is necessary at all or not (like it's not a crash or anything, just not intended behavior at the very least), i'm more into the first, thoughts ?


Regards,
Ayoub Kazar
From 9388407fe911435bc9b473f522233a4c3526466f Mon Sep 17 00:00:00 2001
From: AyoubKAZ <[email protected]>
Date: Thu, 5 Nov 2026 00:43:21 +0100
Subject: [PATCH] file_fdw: Enforce REJECT_LIMIT during ANALYZE

When a file_fdw foreign table has on_error 'ignore' and reject_limit
specified, fileIterateForeignScan() aborts with an error once the number
of skipped rows exceeds reject_limit. However, file_acquire_sample_rows()
failed to check reject_limit when encountering soft errors, allowing
ANALYZE to ignore unlimited erroneous rows without error.

Fix by checking reject_limit after resetting error_occurred in
file_acquire_sample_rows(), just like we already do in fileIterateForeignScan() and
CopyFrom().
---
 contrib/file_fdw/expected/file_fdw.out | 3 +++
 contrib/file_fdw/file_fdw.c            | 7 +++++++
 contrib/file_fdw/sql/file_fdw.sql      | 1 +
 3 files changed, 11 insertions(+)

diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out
index 640986528ae..548961ee4cc 100644
--- a/contrib/file_fdw/expected/file_fdw.out
+++ b/contrib/file_fdw/expected/file_fdw.out
@@ -277,6 +277,9 @@ ALTER FOREIGN TABLE agg_bad OPTIONS (ADD reject_limit '1'); -- ERROR
 SELECT * FROM agg_bad;
 ERROR:  skipped more than REJECT_LIMIT (1) rows due to data type incompatibility
 CONTEXT:  COPY agg_bad, line 5, column b: "bbb"
+ANALYZE agg_bad;
+ERROR:  skipped more than REJECT_LIMIT (1) rows due to data type incompatibility
+CONTEXT:  COPY agg_bad, line 5, column b: "bbb"
 ALTER FOREIGN TABLE agg_bad OPTIONS (SET reject_limit '2');
 SELECT * FROM agg_bad;
   a  |   b    
diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 7d9dc3baf74..b4e7b286a08 100644
--- a/contrib/file_fdw/file_fdw.c
+++ b/contrib/file_fdw/file_fdw.c
@@ -1264,6 +1264,13 @@ file_acquire_sample_rows(Relation onerel, int elevel,
 			 */
 			cstate->escontext->error_occurred = false;
 
+			if (cstate->opts.reject_limit > 0 &&
+				cstate->num_errors > cstate->opts.reject_limit)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+						 errmsg("skipped more than REJECT_LIMIT (%" PRId64 ") rows due to data type incompatibility",
+								cstate->opts.reject_limit)));
+
 			/* Repeat NextCopyFrom() until no soft error occurs */
 			continue;
 		}
diff --git a/contrib/file_fdw/sql/file_fdw.sql b/contrib/file_fdw/sql/file_fdw.sql
index 56bfc926c00..3ebc9af1448 100644
--- a/contrib/file_fdw/sql/file_fdw.sql
+++ b/contrib/file_fdw/sql/file_fdw.sql
@@ -179,6 +179,7 @@ ALTER FOREIGN TABLE agg_bad OPTIONS (ADD log_verbosity 'silent');
 SELECT * FROM agg_bad;
 ALTER FOREIGN TABLE agg_bad OPTIONS (ADD reject_limit '1'); -- ERROR
 SELECT * FROM agg_bad;
+ANALYZE agg_bad;
 ALTER FOREIGN TABLE agg_bad OPTIONS (SET reject_limit '2');
 SELECT * FROM agg_bad;
 ANALYZE agg_bad;
-- 
2.34.1

Reply via email to