diff --git a/contrib/file_fdw/data/text.csv b/contrib/file_fdw/data/text.csv index ...bd4c327 . *** a/contrib/file_fdw/data/text.csv --- b/contrib/file_fdw/data/text.csv *************** *** 0 **** --- 1,4 ---- + 123,123 + abc,abc + NULL,NULL + ABC,ABC diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c index 224e74f..548dcd2 100644 *** a/contrib/file_fdw/file_fdw.c --- b/contrib/file_fdw/file_fdw.c *************** *** 23,30 **** --- 23,32 ---- #include "foreign/fdwapi.h" #include "foreign/foreign.h" #include "miscadmin.h" + #include "nodes/makefuncs.h" #include "optimizer/cost.h" #include "utils/rel.h" + #include "utils/syscache.h" PG_MODULE_MAGIC; *************** static struct FileFdwOption valid_option *** 57,72 **** {"escape", ForeignTableRelationId}, {"null", ForeignTableRelationId}, {"encoding", ForeignTableRelationId}, /* * force_quote is not supported by file_fdw because it's for COPY TO. */ - /* - * force_not_null is not supported by file_fdw. It would need a parser - * for list of columns, not to mention a way to check the column list - * against the table. - */ /* Sentinel */ {NULL, InvalidOid} --- 59,70 ---- {"escape", ForeignTableRelationId}, {"null", ForeignTableRelationId}, {"encoding", ForeignTableRelationId}, + {"force_not_null", AttributeRelationId}, /* specified as boolean value */ /* * force_quote is not supported by file_fdw because it's for COPY TO. */ /* Sentinel */ {NULL, InvalidOid} *************** static void fileGetOptions(Oid foreignta *** 112,117 **** --- 110,116 ---- static void estimate_costs(PlannerInfo *root, RelOptInfo *baserel, const char *filename, Cost *startup_cost, Cost *total_cost); + static List * get_force_not_null(Oid relid); /* *************** file_fdw_validator(PG_FUNCTION_ARGS) *** 145,150 **** --- 144,150 ---- List *options_list = untransformRelOptions(PG_GETARG_DATUM(0)); Oid catalog = PG_GETARG_OID(1); char *filename = NULL; + char *force_not_null = NULL; List *other_options = NIL; ListCell *cell; *************** file_fdw_validator(PG_FUNCTION_ARGS) *** 198,204 **** buf.data))); } ! /* Separate out filename, since ProcessCopyOptions won't allow it */ if (strcmp(def->defname, "filename") == 0) { if (filename) --- 198,207 ---- buf.data))); } ! /* ! * Separate out filename and force_not_null, since ProcessCopyOptions ! * won't allow them. ! */ if (strcmp(def->defname, "filename") == 0) { if (filename) *************** file_fdw_validator(PG_FUNCTION_ARGS) *** 207,212 **** --- 210,229 ---- errmsg("conflicting or redundant options"))); filename = defGetString(def); } + else if (strcmp(def->defname, "force_not_null") == 0) + { + if (force_not_null) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"))); + + force_not_null = defGetString(def); + if (strcmp(force_not_null, "true") != 0 && + strcmp(force_not_null, "false") != 0) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("force_not_null must be true or false"))); + } else other_options = lappend(other_options, def); } *************** is_valid_option(const char *option, Oid *** 245,254 **** --- 262,355 ---- } /* + * Retrieve per-column generic options from pg_attribute and construct a list + * of column names for "force_not_null". + */ + static List * + get_force_not_null(Oid relid) + { + Relation rel; + TupleDesc tupleDesc; + AttrNumber natts; + AttrNumber attnum; + List *columns = NIL; + + rel = heap_open(relid, AccessShareLock); + tupleDesc = RelationGetDescr(rel); + natts = tupleDesc->natts; + + /* Retrieve FDW options from every user-defined attributes. */ + for (attnum = 1; attnum < natts; attnum++) + { + HeapTuple tuple; + Form_pg_attribute attr; + Datum datum; + bool isnull; + List *options; + ListCell *cell; + + + tuple = SearchSysCache2(ATTNUM, + RelationGetRelid(rel), + Int16GetDatum(attnum)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, + "cache lookup failed for attribute %d of relation %u", + attnum, RelationGetRelid(rel)); + attr = (Form_pg_attribute) GETSTRUCT(tuple); + + /* Skip dropped attributes. */ + if (attr->attisdropped) + { + ReleaseSysCache(tuple); + continue; + } + + datum = SysCacheGetAttr(ATTNUM, + tuple, + Anum_pg_attribute_attfdwoptions, + &isnull); + if (isnull) + datum = PointerGetDatum(NULL); + options = untransformRelOptions(datum); + + /* + * Find force_not_null option and append attname to the list if + * the value was true. + */ + foreach (cell, options) + { + DefElem *def = (DefElem *) lfirst(cell); + const char *value = defGetString(def); + + if (strcmp(def->defname, "force_not_null") == 0 && + strcmp(value, "true") == 0) + { + columns = lappend(columns, makeString(NameStr(attr->attname))); + elog(DEBUG1, "%s: force_not_null", NameStr(attr->attname)); + } + + } + + ReleaseSysCache(tuple); + } + + heap_close(rel, AccessShareLock); + + /* Return DefElem only when any column is set "force_not_null=true". */ + if (columns != NIL) + return list_make1(makeDefElem("force_not_null", (Node *) columns)); + else + return NIL; + } + + /* * Fetch the options for a file_fdw foreign table. * * We have to separate out "filename" from the other options because * it must not appear in the options list passed to the core COPY code. + * And we must construct List of DefElem from pg_attribute.attfdwoptions for + * "force_not_null". */ static void fileGetOptions(Oid foreigntableid, *************** fileGetOptions(Oid foreigntableid, *** 296,301 **** --- 397,405 ---- prev = lc; } + /* Retrieve force_not_null from pg_attribute and append it to the list. */ + options = list_concat(options, get_force_not_null(foreigntableid)); + /* * The validator should have checked that a filename was included in the * options, but check again, just in case. diff --git a/contrib/file_fdw/input/file_fdw.source b/contrib/file_fdw/input/file_fdw.source index 1405752..5d7347f 100644 *** a/contrib/file_fdw/input/file_fdw.source --- b/contrib/file_fdw/input/file_fdw.source *************** CREATE FOREIGN TABLE agg_bad ( *** 78,83 **** --- 78,97 ---- ) SERVER file_server OPTIONS (format 'csv', filename '@abs_srcdir@/data/agg.bad', header 'true', delimiter ';', quote '@', escape '"', null ''); + -- per-column options tests + ALTER FOREIGN DATA WRAPPER file_fdw OPTIONS (ADD force_not_null '*'); -- ERROR + ALTER SERVER file_server OPTIONS (ADD force_not_null '*'); -- ERROR + CREATE USER MAPPING FOR public SERVER file_server OPTIONS (force_not_null '*'); -- ERROR + CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (force_not_null '*'); -- ERROR + CREATE FOREIGN TABLE text_csv ( + word1 text OPTIONS (force_not_null 'true'), + word2 text OPTIONS (force_not_null 'false') + ) SERVER file_server + OPTIONS (format 'text', filename '@abs_srcdir@/data/text.csv', null 'NULL'); + SELECT * FROM text_csv ORDER BY word1; -- ERROR + ALTER FOREIGN TABLE text_csv OPTIONS (SET format 'csv'); + SELECT * FROM text_csv ORDER BY word1; + -- basic query tests SELECT * FROM agg_text WHERE b > 10.0 ORDER BY a; SELECT * FROM agg_csv ORDER BY a; diff --git a/contrib/file_fdw/output/file_fdw.source b/contrib/file_fdw/output/file_fdw.source index 6dd2653..cf2cba5 100644 *** a/contrib/file_fdw/output/file_fdw.source --- b/contrib/file_fdw/output/file_fdw.source *************** CREATE FOREIGN TABLE agg_bad ( *** 93,98 **** --- 93,128 ---- b float4 ) SERVER file_server OPTIONS (format 'csv', filename '@abs_srcdir@/data/agg.bad', header 'true', delimiter ';', quote '@', escape '"', null ''); + -- per-column options tests + ALTER FOREIGN DATA WRAPPER file_fdw OPTIONS (ADD force_not_null '*'); -- ERROR + ERROR: invalid option "force_not_null" + HINT: Valid options in this context are: + ALTER SERVER file_server OPTIONS (ADD force_not_null '*'); -- ERROR + ERROR: invalid option "force_not_null" + HINT: Valid options in this context are: + CREATE USER MAPPING FOR public SERVER file_server OPTIONS (force_not_null '*'); -- ERROR + 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 + CREATE FOREIGN TABLE text_csv ( + word1 text OPTIONS (force_not_null 'true'), + word2 text OPTIONS (force_not_null 'false') + ) SERVER file_server + OPTIONS (format 'text', filename '@abs_srcdir@/data/text.csv', null 'NULL'); + SELECT * FROM text_csv ORDER BY word1; -- ERROR + ERROR: COPY force not null available only in CSV mode + ALTER FOREIGN TABLE text_csv OPTIONS (SET format 'csv'); + SELECT * FROM text_csv ORDER BY word1; + word1 | word2 + -------+------- + 123 | 123 + ABC | ABC + NULL | + abc | abc + (4 rows) + -- basic query tests SELECT * FROM agg_text WHERE b > 10.0 ORDER BY a; a | b *************** SET ROLE file_fdw_superuser; *** 216,222 **** -- cleanup RESET ROLE; DROP EXTENSION file_fdw CASCADE; ! NOTICE: drop cascades to 7 other objects DETAIL: drop cascades to server file_server drop cascades to user mapping for file_fdw_user drop cascades to user mapping for file_fdw_superuser --- 246,252 ---- -- cleanup RESET ROLE; DROP EXTENSION file_fdw CASCADE; ! NOTICE: drop cascades to 8 other objects DETAIL: drop cascades to server file_server drop cascades to user mapping for file_fdw_user drop cascades to user mapping for file_fdw_superuser *************** drop cascades to user mapping for no_pri *** 224,227 **** --- 254,258 ---- drop cascades to foreign table agg_text drop cascades to foreign table agg_csv drop cascades to foreign table agg_bad + drop cascades to foreign table text_csv DROP ROLE file_fdw_superuser, file_fdw_user, no_priv_user; diff --git a/doc/src/sgml/file-fdw.sgml b/doc/src/sgml/file-fdw.sgml index 8497d9a..5b35451 100644 *** a/doc/src/sgml/file-fdw.sgml --- b/doc/src/sgml/file-fdw.sgml *************** *** 111,124 **** ! COPY's OIDS, FORCE_QUOTE, ! and FORCE_NOT_NULL options are currently not supported by file_fdw. ! These options can only be specified for a foreign table, not in the ! options of the file_fdw foreign-data wrapper, nor in the options of a server or user mapping using the wrapper. --- 111,147 ---- ! A column of a foreign table created using this wrapper can have the ! following options: ! ! ! ! ! ! force_not_null ! ! ! ! Specifies whether values for the column shouldn't been matched against ! the null string. Acceptable values are true for no matching, ! and false for matching (case sensitive). ! true is same as specifying the column in COPY's ! FORCE_NOT_NULL option. ! ! ! ! ! ! ! ! COPY's OIDS and ! FORCE_QUOTE options are currently not supported by file_fdw. ! These options can only be specified for a foreign table or its columns, not ! in the options of the file_fdw foreign-data wrapper, nor in the options of a server or user mapping using the wrapper.