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