diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c index 2d36a72..f796a54 100644 --- a/contrib/file_fdw/file_fdw.c +++ b/contrib/file_fdw/file_fdw.c @@ -71,6 +71,9 @@ static const struct FileFdwOption valid_options[] = { * force_quote is not supported by file_fdw because it's for COPY TO. */ + /* Validation options */ + {"validate", ForeignTableRelationId}, + /* Sentinel */ {NULL, InvalidOid} }; @@ -93,6 +96,7 @@ typedef struct FileFdwExecutionState { char *filename; /* file to read */ List *options; /* merged COPY options, excluding filename */ + bool validate; /* whether to validate NOT NULL constraint */ CopyState cstate; /* state of reading file */ } FileFdwExecutionState; @@ -134,7 +138,9 @@ static bool fileAnalyzeForeignTable(Relation relation, */ static bool is_valid_option(const char *option, Oid context); static void fileGetOptions(Oid foreigntableid, - char **filename, List **other_options); + char **filename, + List **other_options, + bool *validate); static List *get_file_fdw_attribute_options(Oid relid); static void estimate_size(PlannerInfo *root, RelOptInfo *baserel, FileFdwPlanState *fdw_private); @@ -180,6 +186,7 @@ file_fdw_validator(PG_FUNCTION_ARGS) List *options_list = untransformRelOptions(PG_GETARG_DATUM(0)); Oid catalog = PG_GETARG_OID(1); char *filename = NULL; + DefElem *validate = NULL; DefElem *force_not_null = NULL; List *other_options = NIL; ListCell *cell; @@ -247,6 +254,16 @@ file_fdw_validator(PG_FUNCTION_ARGS) errmsg("conflicting or redundant options"))); filename = defGetString(def); } + else if (strcmp(def->defname, "validate") == 0) + { + if (validate) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"))); + validate = def; + /* Don't care what the value is, as long as it's a legal boolean */ + (void) defGetBoolean(def); + } else if (strcmp(def->defname, "force_not_null") == 0) { if (force_not_null) @@ -301,8 +318,8 @@ is_valid_option(const char *option, Oid context) * it must not appear in the options list passed to the core COPY code. */ static void -fileGetOptions(Oid foreigntableid, - char **filename, List **other_options) +fileGetOptions(Oid foreigntableid, char **filename, + List **other_options, bool *validate) { ForeignTable *table; ForeignServer *server; @@ -333,6 +350,7 @@ fileGetOptions(Oid foreigntableid, * Separate out the filename. */ *filename = NULL; + *validate = false; prev = NULL; foreach(lc, options) { @@ -342,7 +360,11 @@ fileGetOptions(Oid foreigntableid, { *filename = defGetString(def); options = list_delete_cell(options, lc, prev); - break; + } + if (strcmp(def->defname, "validate") == 0) + { + *validate = defGetBoolean(def); + options = list_delete_cell(options, lc, prev); } prev = lc; } @@ -425,6 +447,7 @@ fileGetForeignRelSize(PlannerInfo *root, Oid foreigntableid) { FileFdwPlanState *fdw_private; + bool validate; /* * Fetch options. We only need filename at this point, but we might @@ -432,7 +455,9 @@ fileGetForeignRelSize(PlannerInfo *root, */ fdw_private = (FileFdwPlanState *) palloc(sizeof(FileFdwPlanState)); fileGetOptions(foreigntableid, - &fdw_private->filename, &fdw_private->options); + &fdw_private->filename, + &fdw_private->options, + &validate); baserel->fdw_private = (void *) fdw_private; /* Estimate relation size */ @@ -517,10 +542,11 @@ fileExplainForeignScan(ForeignScanState *node, ExplainState *es) { char *filename; List *options; + bool validate; /* Fetch options --- we only need filename at this point */ fileGetOptions(RelationGetRelid(node->ss.ss_currentRelation), - &filename, &options); + &filename, &options, &validate); ExplainPropertyText("Foreign File", filename, es); @@ -544,6 +570,7 @@ fileBeginForeignScan(ForeignScanState *node, int eflags) { char *filename; List *options; + bool validate; CopyState cstate; FileFdwExecutionState *festate; @@ -555,7 +582,7 @@ fileBeginForeignScan(ForeignScanState *node, int eflags) /* Fetch options of foreign table */ fileGetOptions(RelationGetRelid(node->ss.ss_currentRelation), - &filename, &options); + &filename, &options, &validate); /* * Create CopyState from FDW options. We always acquire all columns, so @@ -573,6 +600,7 @@ fileBeginForeignScan(ForeignScanState *node, int eflags) festate = (FileFdwExecutionState *) palloc(sizeof(FileFdwExecutionState)); festate->filename = filename; festate->options = options; + festate->validate = validate; festate->cstate = cstate; node->fdw_state = (void *) festate; @@ -588,6 +616,8 @@ fileIterateForeignScan(ForeignScanState *node) { FileFdwExecutionState *festate = (FileFdwExecutionState *) node->fdw_state; TupleTableSlot *slot = node->ss.ss_ScanTupleSlot; + Relation rel = node->ss.ss_currentRelation; + TupleConstr *constr = rel->rd_att->constr; bool found; ErrorContextCallback errcontext; @@ -614,7 +644,11 @@ fileIterateForeignScan(ForeignScanState *node) slot->tts_values, slot->tts_isnull, NULL); if (found) + { ExecStoreVirtualTuple(slot); + if (constr && constr->has_not_null && festate->validate) + ExecNotNullCheck(rel, slot); + } /* Remove error callback. */ error_context_stack = errcontext.previous; @@ -664,10 +698,11 @@ fileAnalyzeForeignTable(Relation relation, { char *filename; List *options; + bool validate; struct stat stat_buf; /* Fetch options of foreign table */ - fileGetOptions(RelationGetRelid(relation), &filename, &options); + fileGetOptions(RelationGetRelid(relation), &filename, &options, &validate); /* * Get size of the file. (XXX if we fail here, would it be better to @@ -831,6 +866,7 @@ file_acquire_sample_rows(Relation onerel, int elevel, bool found; char *filename; List *options; + bool validate; CopyState cstate; ErrorContextCallback errcontext; MemoryContext oldcontext = CurrentMemoryContext; @@ -844,7 +880,7 @@ file_acquire_sample_rows(Relation onerel, int elevel, nulls = (bool *) palloc(tupDesc->natts * sizeof(bool)); /* Fetch options of foreign table */ - fileGetOptions(RelationGetRelid(onerel), &filename, &options); + fileGetOptions(RelationGetRelid(onerel), &filename, &options, &validate); /* * Create CopyState from FDW options. diff --git a/contrib/file_fdw/output/file_fdw.source b/contrib/file_fdw/output/file_fdw.source index 84f0750..017f193 100644 --- a/contrib/file_fdw/output/file_fdw.source +++ b/contrib/file_fdw/output/file_fdw.source @@ -123,7 +123,7 @@ ERROR: invalid option "force_not_null" HINT: Valid options in this context are: CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (force_not_null '*'); -- ERROR ERROR: invalid option "force_not_null" -HINT: Valid options in this context are: filename, format, header, delimiter, quote, escape, null, encoding +HINT: Valid options in this context are: filename, format, header, delimiter, quote, escape, null, encoding, validate -- basic query tests SELECT * FROM agg_text WHERE b > 10.0 ORDER BY a; a | b diff --git a/doc/src/sgml/file-fdw.sgml b/doc/src/sgml/file-fdw.sgml index 4c7f7e8..d6b474b 100644 --- a/doc/src/sgml/file-fdw.sgml +++ b/doc/src/sgml/file-fdw.sgml @@ -108,6 +108,16 @@ + + validate + + + + Specifies whether the NOT NULL constraint is validated. + + + + diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index fbb36fa..5417eea 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -80,7 +80,7 @@ static void ExecutePlan(EState *estate, PlanState *planstate, static bool ExecCheckRTEPerms(RangeTblEntry *rte); static void ExecCheckXactReadOnly(PlannedStmt *plannedstmt); static char *ExecBuildSlotValueDescription(TupleTableSlot *slot, - int maxfieldlen); + int maxfieldlen); static void EvalPlanQualStart(EPQState *epqstate, EState *parentestate, Plan *planTree); @@ -1438,6 +1438,28 @@ ExecutePlan(EState *estate, /* + * ExecNotNullCheck --- check that tuple meets NOT NULL constraints + */ +void +ExecNotNullCheck(Relation rel, TupleTableSlot *slot) +{ + int natts = rel->rd_att->natts; + int attrChk; + + for (attrChk = 1; attrChk <= natts; attrChk++) + { + if (rel->rd_att->attrs[attrChk - 1]->attnotnull && + slot_attisnull(slot, attrChk)) + ereport(ERROR, + (errcode(ERRCODE_NOT_NULL_VIOLATION), + errmsg("null value in column \"%s\" violates not-null constraint", + NameStr(rel->rd_att->attrs[attrChk - 1]->attname)), + errdetail("Failing row contains %s.", + ExecBuildSlotValueDescription(slot, 64)))); + } +} + +/* * ExecRelCheck --- check that tuple meets constraints for result relation */ static const char * @@ -1509,22 +1531,7 @@ ExecConstraints(ResultRelInfo *resultRelInfo, Assert(constr); if (constr->has_not_null) - { - int natts = rel->rd_att->natts; - int attrChk; - - for (attrChk = 1; attrChk <= natts; attrChk++) - { - if (rel->rd_att->attrs[attrChk - 1]->attnotnull && - slot_attisnull(slot, attrChk)) - ereport(ERROR, - (errcode(ERRCODE_NOT_NULL_VIOLATION), - errmsg("null value in column \"%s\" violates not-null constraint", - NameStr(rel->rd_att->attrs[attrChk - 1]->attname)), - errdetail("Failing row contains %s.", - ExecBuildSlotValueDescription(slot, 64)))); - } - } + ExecNotNullCheck(rel, slot); if (constr->num_check > 0) { @@ -1548,7 +1555,7 @@ ExecConstraints(ResultRelInfo *resultRelInfo, * here since heap field values could be very long, whereas index entries * typically aren't so wide. */ -static char * +char * ExecBuildSlotValueDescription(TupleTableSlot *slot, int maxfieldlen) { StringInfoData buf; diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index f5503a5..5eda8ea 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -188,6 +188,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo, int instrument_options); extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid); extern bool ExecContextForcesOids(PlanState *planstate, bool *hasoids); +extern void ExecNotNullCheck(Relation rel, TupleTableSlot *slot); extern void ExecConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, EState *estate); extern ExecRowMark *ExecFindRowMark(EState *estate, Index rti);