per-column generic option

Started by Shigeru Hanadaover 14 years ago31 messages
#1Shigeru Hanada
shigeru.hanada@gmail.com
1 attachment(s)

Hi,

I would like to propose support for per-column generic option, which is
defined in the SQL/MED standard. In 9.0 release, support for foreign
tables and per-table generic option have been added, but support for
per-column generic option hasn't.

Please examine the description below and attached patch
per_column_option_v1.patch. Any comments or questions are welcome.

Possible use cases
~~~~~~~~~~~~~~~~~~
Purpose of per-column generic option is passing column-specific settings
to the foreign-data wrapper.

1) For file_fdw, per-column generic option can be used to represent
per-column COPY option FORCE_NOT_NULL with boolean value (currently
file_fdw doesn't support FORCE_NOT_NULL option).

2) For postgresql_fdw (even though it has not been implemented yet),
per-column generic option could be used to represent the name of the
column on the foreign side. It is similar to per-table generic option
such as "nspname" and "relname" for namespace name/relation name,
proposed in the last development cycles. Such option would be named
"attname" after pg_attribute.attname.

Catalog design
~~~~~~~~~~~~~~
This proposal requires changing some catalogs.

1) To store per-column generic options, new attribute attfdwoptions
(text[]) was added at tail of pg_attribute. This is similar to the
generic option of other FDW objects such as FDW, server, user mapping
and foreign table. Existing attribute attoptions is not used for
generic options.

2) To conform the SQL/MED standard, an information_schema view
COLUMN_OPTIONS was added. Also underlying view
_pg_foreign_table_columns was added to show only columns which current
user has any access privilege. This fashion is same as other FDW views.

Syntax design
~~~~~~~~~~~~~
Per-column generic options can be operated via CREATE FOREIGN TABLE
statement and ALTER FOREIGN TABLE statement. Similar to other generic
options, ADD/SET/DROP can be specified for ALTER FOREIGN TABLE.

1) In CREATE FOREIGN TABLE statement, per-column generic options can be
specified in a column definition without operation qualifier such as
SET, ADD and DROP; all options are treated as ADD. Similar to other FDW
objects, multiple options can be specified for one column by separating
option-value pairs with comma.

-- multiple options can be specified for one column at once
CREATE FOREIGN TABLE foo (
c1 int OPTIONS (opt1 'value1'),
c2 text OPTIONS (opt2 'values2', opt3 'value3'),
c3 date OPTIONS (opt4 'value4) NOT NULL
) SERVER server;

To avoid syntax conflict between "OPTIONS (...)" and "DEFAULT b_expr"
(b_expr can end with a token "OPTION"), I placed OPTIONS (...) between
data type and any other column qualifier such as default values and
constraints.

The SQL/MED standard doesn't consider any column qualifier other than
data type, so it defines the syntax simply as below. I think new syntax
conforms the standard...

CREATE FOREIGN TABLE foo (
{ column_name data_type [ OPTIONS ( option 'value' [, ...] ) ] }
[, ... ]
) SERVER server [ OPTIONS (...) ]

Please note that CREATE FOREIGN TABLE shares the columnDef, a syntax
element for a column definition, with CREATE TABLE. I thought that they
should so, and I didn't introduce separated syntax for foreign tables.

2) Similar to other FDW objects' ALTER statement, ALTER FOREIGN TABLE
ALTER COLUMN accepts ADD/SET/DROP operation for each option. DROP
requires only option name.

ALTER FOREIGN TABLE foo ALTER COLUMN c1
OPTIONS (SET opt1 'VALUE1'); -- should be set in advance
ALTER FOREIGN TABLE foo ALTER COLUMN c1
OPTIONS (ADD opt2 'VALUE1', DROP opt1);

Similar to other ALTER FOREIGN TABLE commands, ALTER COLUMN ... OPTIONS
(...) can be contained in the list of ALTER commands.

ALTER FOREIGN TABLE foo
ALTER COLUMN col1 OPTIONS (opt1 'val1'),
ALTER COLUMN col2 SET NOT NULL;

psql support
~~~~~~~~~~~~
1) psql should support describing per-column generic options, so \dec
command was added. If the form \dec+ is used, generic options are also
displayed. Output sample is:

postgres=# \dec csv_branches
List of foreign table columns
Schema | Table | Column
--------+--------------+----------
public | csv_branches | bid
public | csv_branches | bbalance
public | csv_branches | filler
(3 rows)

postgres=# \dec+ csv_branches
List of foreign table columns
Schema | Table | Column | Options
--------+--------------+----------+------------------------
public | csv_branches | bid | {force_not_null=false}
public | csv_branches | bbalance | {force_not_null=true}
public | csv_branches | filler |
(3 rows)

Here I found an inconsistency about privilege to see generic options
(not only column but also FDW and server et al). The
information_schema.*_options only shows options which are associated to
objects that current user can access, but \de*+ doesn't have such
restriction. \de* commands should be fixed to hide forbidden objects?

2) psql can support tab-completion CREATE/ALTER FOREIGN TABLE statement
about OPTIONS, but the patch doesn't include this feature.

pg_dump support
~~~~~~~~~~~~~~~
Sorry, I overlooked this issue till writing this post... I'm going to
work on this and post revised patch soon. Please examine other parts first.

Documents
~~~~~~~~~
1) Is "generic options" proper term to mean FDW-specific option
associated to a FDW object? It's used in the SQL/MED standard, but
seems not popular... "FDW option" would be better than "generic option"?

Regards,
--
Shigeru Hanada

Attachments:

per_column_option_v1.patchtext/plain; name=per_column_option_v1.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 8504555..d1225e1 100644
*** a/doc/src/sgml/catalogs.sgml
--- b/doc/src/sgml/catalogs.sgml
***************
*** 1152,1157 ****
--- 1152,1166 ----
        </entry>
       </row>
  
+      <row>
+       <entry><structfield>attfdwoptions</structfield></entry>
+       <entry><type>text[]</type></entry>
+       <entry></entry>
+       <entry>
+        Attribute-level generic options, as <quote>keyword=value</> strings
+       </entry>
+      </row>
+ 
      </tbody>
     </tgroup>
    </table>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 6df69db..539f573 100644
*** a/doc/src/sgml/information_schema.sgml
--- b/doc/src/sgml/information_schema.sgml
***************
*** 958,963 ****
--- 958,1026 ----
    </table>
   </sect1>
  
+  <sect1 id="infoschema-column-options">
+   <title><literal>column_options</literal></title>
+ 
+   <para>
+    The view <literal>column_options</literal> contains all the
+    options defined for foreign table columns in the current database.  Only
+    those foreign table columns are shown that the current user has access to
+    (by way of being the owner or having some privilege).
+   </para>
+ 
+   <table>
+    <title><literal>column_options</literal> Columns</title>
+ 
+    <tgroup cols="3">
+     <thead>
+      <row>
+       <entry>Name</entry>
+       <entry>Data Type</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+ 
+     <tbody>
+      <row>
+       <entry><literal>table_catalog</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the database that contains the foreign table (always the current database)</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>table_schema</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the schema that contains the foreign table</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>table_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the foreign table</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>column_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the column</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>option_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of an option</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>option_value</literal></entry>
+       <entry><type>character_data</type></entry>
+       <entry>Value of the option</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect1>
+ 
   <sect1 id="infoschema-column-privileges">
    <title><literal>column_privileges</literal></title>
  
diff --git a/doc/src/sgml/ref/alter_foreign_table.sgml b/doc/src/sgml/ref/alter_foreign_table.sgml
index a45df02..95ae02a 100644
*** a/doc/src/sgml/ref/alter_foreign_table.sgml
--- b/doc/src/sgml/ref/alter_foreign_table.sgml
*************** ALTER FOREIGN TABLE <replaceable class="
*** 36,41 ****
--- 36,42 ----
      DROP [ COLUMN ] [ IF EXISTS ] <replaceable class="PARAMETER">column</replaceable> [ RESTRICT | CASCADE ]
      ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> [ SET DATA ] TYPE <replaceable class="PARAMETER">type</replaceable>
      ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> { SET | DROP } NOT NULL
+     ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
      OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
      OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
  </synopsis>
*************** ALTER FOREIGN TABLE <replaceable class="
*** 125,136 ****
      <term><literal>OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ] )</literal></term>
      <listitem>
       <para>
!       Change options for the foreign table.
        <literal>ADD</>, <literal>SET</>, and <literal>DROP</>
        specify the action to be performed.  <literal>ADD</> is assumed
!       if no operation is explicitly specified.  Option names must be
!       unique; names and values are also validated using the foreign
!       data wrapper library.
       </para>
      </listitem>
     </varlistentry>
--- 126,137 ----
      <term><literal>OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ] )</literal></term>
      <listitem>
       <para>
!       Change options for the foreign table or the column of the foreign table.
        <literal>ADD</>, <literal>SET</>, and <literal>DROP</>
        specify the action to be performed.  <literal>ADD</> is assumed
!       if no operation is explicitly specified.  Option names must be unique
!       in each associated object; names and values are also validated using the
!       foreign data wrapper library.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml
index ad91072..8863386 100644
*** a/doc/src/sgml/ref/create_foreign_table.sgml
--- b/doc/src/sgml/ref/create_foreign_table.sgml
***************
*** 19,25 ****
   <refsynopsisdiv>
  <synopsis>
  CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="PARAMETER">table_name</replaceable> ( [
!   { <replaceable class="PARAMETER">column_name</replaceable> <replaceable class="PARAMETER">data_type</replaceable> [ NULL | NOT NULL ] }
      [, ... ]
  ] )
    SERVER <replaceable class="parameter">server_name</replaceable>
--- 19,25 ----
   <refsynopsisdiv>
  <synopsis>
  CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="PARAMETER">table_name</replaceable> ( [
!   { <replaceable class="PARAMETER">column_name</replaceable> <replaceable class="PARAMETER">data_type</replaceable> [ OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ... ] ) ] [ NULL | NOT NULL ] }
      [, ... ]
  ] )
    SERVER <replaceable class="parameter">server_name</replaceable>
*************** CREATE FOREIGN TABLE [ IF NOT EXISTS ] <
*** 138,147 ****
      <term><literal>OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ...] )</literal></term>
      <listitem>
       <para>
!       Options to be associated with the new foreign table.
        The allowed option names and values are specific to each foreign
        data wrapper and are validated using the foreign-data wrapper's
!       validator function. Option names must be unique.
       </para>
      </listitem>
     </varlistentry>
--- 138,149 ----
      <term><literal>OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ...] )</literal></term>
      <listitem>
       <para>
!       Options to be associated with the new foreign table or the column of
!       the foreign table.
        The allowed option names and values are specific to each foreign
        data wrapper and are validated using the foreign-data wrapper's
!       validator function. Option names must be unique in each associated
!       object.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index eaf901d..e9e9a61 100644
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** testdb=&gt;
*** 1109,1114 ****
--- 1109,1128 ----
  
  
        <varlistentry>
+         <term><literal>\dec[+] [ <link linkend="APP-PSQL-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+         <listitem>
+         <para>
+         Lists foreign table columns (mnemonic: <quote>external columns</quote>).
+         If <replaceable class="parameter">pattern</replaceable> is
+         specified, only entries whose table name or schema name matches
+         the pattern are listed.  If the form <literal>\dec+</literal>
+         is used, generic options are also displayed.
+         </para>
+         </listitem>
+       </varlistentry>
+ 
+ 
+       <varlistentry>
          <term><literal>\deu[+] [ <link linkend="APP-PSQL-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
          <listitem>
          <para>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 16979c4..4ffedab 100644
*** a/src/backend/access/common/tupdesc.c
--- b/src/backend/access/common/tupdesc.c
*************** equalTupleDescs(TupleDesc tupdesc1, Tupl
*** 362,368 ****
  			return false;
  		if (attr1->attcollation != attr2->attcollation)
  			return false;
! 		/* attacl and attoptions are not even present... */
  	}
  
  	if (tupdesc1->constr != NULL)
--- 362,368 ----
  			return false;
  		if (attr1->attcollation != attr2->attcollation)
  			return false;
! 		/* attacl, attoptions and attfdwoptions are not even present... */
  	}
  
  	if (tupdesc1->constr != NULL)
*************** TupleDescInitEntry(TupleDesc desc,
*** 482,488 ****
  	att->attisdropped = false;
  	att->attislocal = true;
  	att->attinhcount = 0;
! 	/* attacl and attoptions are not present in tupledescs */
  
  	tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(oidtypeid));
  	if (!HeapTupleIsValid(tuple))
--- 482,488 ----
  	att->attisdropped = false;
  	att->attislocal = true;
  	att->attinhcount = 0;
! 	/* attacl, attoptions and attfdwoptions are not present in tupledescs */
  
  	tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(oidtypeid));
  	if (!HeapTupleIsValid(tuple))
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index 0aeaf5b..d91af52 100644
*** a/src/backend/catalog/genbki.pl
--- b/src/backend/catalog/genbki.pl
*************** sub emit_pgattr_row
*** 369,375 ****
          attislocal    => 't',
          attinhcount   => '0',
          attacl        => '_null_',
!         attoptions    => '_null_'
      );
      return {%PGATTR_DEFAULTS, %row};
  }
--- 369,376 ----
          attislocal    => 't',
          attinhcount   => '0',
          attacl        => '_null_',
!         attoptions    => '_null_',
!         attfdwoptions => '_null_'
      );
      return {%PGATTR_DEFAULTS, %row};
  }
*************** sub emit_schemapg_row
*** 400,405 ****
--- 401,407 ----
      # Only the fixed-size portions of the descriptors are ever used.
      delete $row->{attacl};
      delete $row->{attoptions};
+     delete $row->{attfdwoptions};
  
      # Expand booleans from 'f'/'t' to 'false'/'true'.
      # Some values might be other macros (eg FLOAT4PASSBYVAL), don't change.
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index a6e541d..f3090e2 100644
*** a/src/backend/catalog/heap.c
--- b/src/backend/catalog/heap.c
*************** static List *insert_ordered_unique_oid(L
*** 126,132 ****
   */
  
  /*
!  * The initializers below do not include the attoptions or attacl fields,
   * but that's OK - we're never going to reference anything beyond the
   * fixed-size portion of the structure anyway.
   */
--- 126,132 ----
   */
  
  /*
!  * The initializers below do not include trailing variable length fields,
   * but that's OK - we're never going to reference anything beyond the
   * fixed-size portion of the structure anyway.
   */
*************** InsertPgAttributeTuple(Relation pg_attri
*** 607,612 ****
--- 607,614 ----
  	/* start out with empty permissions and empty options */
  	nulls[Anum_pg_attribute_attacl - 1] = true;
  	nulls[Anum_pg_attribute_attoptions - 1] = true;
+ 	/* FIXME use content of OPTIONS (...) if any. */
+ 	nulls[Anum_pg_attribute_attfdwoptions - 1] = true;
  
  	tup = heap_form_tuple(RelationGetDescr(pg_attribute_rel), values, nulls);
  
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 452a0ea..a8e8a12 100644
*** a/src/backend/catalog/information_schema.sql
--- b/src/backend/catalog/information_schema.sql
*************** GRANT SELECT ON element_types TO PUBLIC;
*** 2449,2454 ****
--- 2449,2487 ----
  
  -- SQL/MED views; these use section numbers from part 9 of the standard.
  
+ /* Base view for foreign table columns */
+ CREATE VIEW _pg_foreign_table_columns AS
+     SELECT n.nspname,
+            c.relname,
+            a.attname,
+            a.attfdwoptions
+     FROM pg_foreign_table t, pg_authid u, pg_namespace n, pg_class c,
+          pg_attribute a
+     WHERE u.oid = c.relowner
+           AND (pg_has_role(c.relowner, 'USAGE')
+                OR has_column_privilege(c.oid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'))
+           AND n.oid = c.relnamespace
+           AND c.oid = t.ftrelid
+           AND c.relkind = 'f'
+           AND a.attrelid = c.oid
+           AND a.attnum > 0;
+ 
+ /*
+  * 24.2
+  * COLUMN_OPTIONS view
+  */
+ CREATE VIEW column_options AS
+     SELECT CAST(current_database() AS sql_identifier) AS table_catalog,
+            c.nspname AS table_schema,
+            c.relname AS table_name,
+            c.attname AS column_name,
+            CAST((pg_options_to_table(c.attfdwoptions)).option_name AS sql_identifier) AS option_name,
+            CAST((pg_options_to_table(c.attfdwoptions)).option_value AS character_data) AS option_value
+     FROM _pg_foreign_table_columns c;
+ 
+ GRANT SELECT ON column_options TO PUBLIC;
+ 
+ 
  /* Base view for foreign-data wrappers */
  CREATE VIEW _pg_foreign_data_wrappers AS
      SELECT w.oid,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2c9f855..d604273 100644
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** static void ATPrepAlterColumnType(List *
*** 343,348 ****
--- 343,349 ----
  static bool ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno);
  static void ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
  					  AlterTableCmd *cmd, LOCKMODE lockmode);
+ static void ATExecAlterColumnGenericOptions(Relation rel, const char *colName, List *options, LOCKMODE lockmode);
  static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode);
  static void ATPostAlterTypeParse(char *cmd, List **wqueue, LOCKMODE lockmode);
  static void change_owner_recurse_to_sequences(Oid relationOid,
*************** AlterTableGetLockLevel(List *cmds)
*** 2606,2611 ****
--- 2607,2613 ----
  			case AT_DropNotNull:		/* may change some SQL plans */
  			case AT_SetNotNull:
  			case AT_GenericOptions:
+ 			case AT_AlterColumnGenericOptions:
  				cmd_lockmode = AccessExclusiveLock;
  				break;
  
*************** ATPrepCmd(List **wqueue, Relation rel, A
*** 2880,2885 ****
--- 2882,2893 ----
  			ATPrepAlterColumnType(wqueue, tab, rel, recurse, recursing, cmd, lockmode);
  			pass = AT_PASS_ALTER_TYPE;
  			break;
+ 		case AT_AlterColumnGenericOptions:
+ 			ATSimplePermissions(rel, ATT_FOREIGN_TABLE);
+ 			/* This command never recurses */
+ 			/* No command-specific prep needed */
+ 			pass = AT_PASS_MISC;
+ 			break;
  		case AT_ChangeOwner:	/* ALTER OWNER */
  			/* This command never recurses */
  			/* No command-specific prep needed */
*************** ATExecCmd(List **wqueue, AlteredTableInf
*** 3113,3118 ****
--- 3121,3129 ----
  		case AT_AlterColumnType:		/* ALTER COLUMN TYPE */
  			ATExecAlterColumnType(tab, rel, cmd, lockmode);
  			break;
+ 		case AT_AlterColumnGenericOptions:	/* ALTER COLUMN OPTIONS */
+ 			ATExecAlterColumnGenericOptions(rel, cmd->name, (List *) cmd->def, lockmode);
+ 			break;
  		case AT_ChangeOwner:	/* ALTER OWNER */
  			ATExecChangeOwner(RelationGetRelid(rel),
  							  get_role_oid(cmd->name, false),
*************** ATExecAlterColumnType(AlteredTableInfo *
*** 7162,7167 ****
--- 7173,7272 ----
  	heap_freetuple(heapTup);
  }
  
+ static void
+ ATExecAlterColumnGenericOptions(Relation rel,
+ 								const char *colName,
+ 								List *options,
+ 								LOCKMODE lockmode)
+ {
+ 	Relation	ftrel;
+ 	Relation	attrel;
+ 	ForeignServer *server;
+ 	ForeignDataWrapper *fdw;
+ 	HeapTuple	tuple;
+ 	HeapTuple	newtuple;
+ 	bool		isnull;
+ 	Datum		repl_val[Natts_pg_attribute];
+ 	bool		repl_null[Natts_pg_attribute];
+ 	bool		repl_repl[Natts_pg_attribute];
+ 	Datum		datum;
+ 	Form_pg_foreign_table fttableform;
+ 	Form_pg_attribute atttableform;
+ 
+ 	if (options == NIL)
+ 		return;
+ 
+ 	/* First, determine FDW validator associated to the foreign table. */
+ 	ftrel = heap_open(ForeignTableRelationId, AccessShareLock);
+ 	tuple = SearchSysCache1(FOREIGNTABLEREL, rel->rd_id);
+ 	if (!HeapTupleIsValid(tuple))
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_UNDEFINED_OBJECT),
+ 				 errmsg("foreign table \"%s\" does not exist",
+ 						RelationGetRelationName(rel))));
+ 	fttableform = (Form_pg_foreign_table) GETSTRUCT(tuple);
+ 	server = GetForeignServer(fttableform->ftserver);
+ 	fdw = GetForeignDataWrapper(server->fdwid);
+ 
+ 	heap_close(ftrel, AccessShareLock);
+ 	ReleaseSysCache(tuple);
+ 
+ 	attrel = heap_open(AttributeRelationId, RowExclusiveLock);
+ 	tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName);
+ 	if (!HeapTupleIsValid(tuple))
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_UNDEFINED_COLUMN),
+ 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+ 						colName, RelationGetRelationName(rel))));
+ 
+ 	/* Prevent them from altering a system attribute */
+ 	atttableform = (Form_pg_attribute) GETSTRUCT(tuple);
+ 	if (atttableform->attnum <= 0)
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ 				 errmsg("cannot alter system column \"%s\"", colName)));
+ 
+ 
+ 	/* Initialize buffers for new tuple values */
+ 	memset(repl_val, 0, sizeof(repl_val));
+ 	memset(repl_null, false, sizeof(repl_null));
+ 	memset(repl_repl, false, sizeof(repl_repl));
+ 
+ 	/* Extract the current options */
+ 	datum = SysCacheGetAttr(ATTNAME,
+ 							tuple,
+ 							Anum_pg_attribute_attfdwoptions,
+ 							&isnull);
+ 	if (isnull)
+ 		datum = PointerGetDatum(NULL);
+ 
+ 	/* Transform the options */
+ 	datum = transformGenericOptions(AttributeRelationId,
+ 									datum,
+ 									options,
+ 									fdw->fdwvalidator);
+ 
+ 	if (PointerIsValid(DatumGetPointer(datum)))
+ 		repl_val[Anum_pg_attribute_attfdwoptions - 1] = datum;
+ 	else
+ 		repl_null[Anum_pg_attribute_attfdwoptions - 1] = true;
+ 
+ 	repl_repl[Anum_pg_attribute_attfdwoptions - 1] = true;
+ 
+ 	/* Everything looks good - update the tuple */
+ 
+ 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrel),
+ 								 repl_val, repl_null, repl_repl);
+ 	ReleaseSysCache(tuple);
+ 
+ 	simple_heap_update(attrel, &newtuple->t_self, newtuple);
+ 	CatalogUpdateIndexes(attrel, newtuple);
+ 
+ 	heap_close(attrel, RowExclusiveLock);
+ 
+ 	heap_freetuple(newtuple);
+ }
+ 
  /*
   * Cleanup after we've finished all the ALTER TYPE operations for a
   * particular relation.  We have to drop and recreate all the indexes
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c9133dd..148110f 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyColumnDef(ColumnDef *from)
*** 2312,2317 ****
--- 2312,2318 ----
  	COPY_NODE_FIELD(collClause);
  	COPY_SCALAR_FIELD(collOid);
  	COPY_NODE_FIELD(constraints);
+ 	COPY_NODE_FIELD(fdwoptions);
  
  	return newnode;
  }
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 681f5f8..cb9be13 100644
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outColumnDef(StringInfo str, ColumnDef 
*** 2101,2106 ****
--- 2101,2107 ----
  	WRITE_NODE_FIELD(collClause);
  	WRITE_OID_FIELD(collOid);
  	WRITE_NODE_FIELD(constraints);
+ 	WRITE_NODE_FIELD(fdwoptions);
  }
  
  static void
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 1d39674..c6c3c39 100644
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** alter_table_cmd:
*** 1760,1765 ****
--- 1760,1774 ----
  					def->raw_default = $8;
  					$$ = (Node *)n;
  				}
+ 			/* ALTER FOREIGN TABLE <name> ALTER [COLUMN] <colname> OPTIONS */
+ 			| ALTER opt_column ColId alter_generic_options
+ 				{
+ 					AlterTableCmd *n = makeNode(AlterTableCmd);
+ 					n->subtype = AT_AlterColumnGenericOptions;
+ 					n->name = $3;
+ 					n->def = (Node *) $4;
+ 					$$ = (Node *)n;
+ 				}
  			/* ALTER TABLE <name> ADD CONSTRAINT ... */
  			| ADD_P TableConstraint
  				{
*************** TypedTableElement:
*** 2488,2494 ****
  			| TableConstraint					{ $$ = $1; }
  		;
  
! columnDef:	ColId Typename ColQualList
  				{
  					ColumnDef *n = makeNode(ColumnDef);
  					n->colname = $1;
--- 2497,2503 ----
  			| TableConstraint					{ $$ = $1; }
  		;
  
! columnDef:	ColId Typename create_generic_options ColQualList
  				{
  					ColumnDef *n = makeNode(ColumnDef);
  					n->colname = $1;
*************** columnDef:	ColId Typename ColQualList
*** 2501,2507 ****
  					n->raw_default = NULL;
  					n->cooked_default = NULL;
  					n->collOid = InvalidOid;
! 					SplitColQualList($3, &n->constraints, &n->collClause,
  									 yyscanner);
  					$$ = (Node *)n;
  				}
--- 2510,2517 ----
  					n->raw_default = NULL;
  					n->cooked_default = NULL;
  					n->collOid = InvalidOid;
! 					n->fdwoptions = $3;
! 					SplitColQualList($4, &n->constraints, &n->collClause,
  									 yyscanner);
  					$$ = (Node *)n;
  				}
*************** AlterFdwStmt: ALTER FOREIGN DATA_P WRAPP
*** 3681,3687 ****
  /* Options definition for CREATE FDW, SERVER and USER MAPPING */
  create_generic_options:
  			OPTIONS '(' generic_option_list ')'			{ $$ = $3; }
! 			| /*EMPTY*/									{ $$ = NIL; }
  		;
  
  generic_option_list:
--- 3691,3697 ----
  /* Options definition for CREATE FDW, SERVER and USER MAPPING */
  create_generic_options:
  			OPTIONS '(' generic_option_list ')'			{ $$ = $3; }
! 			| /*EMPTY*/									{ $$ = NIL }
  		;
  
  generic_option_list:
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index efb8fdd..8deb7af 100644
*** a/src/backend/parser/parse_utilcmd.c
--- b/src/backend/parser/parse_utilcmd.c
*************** transformColumnDefinition(CreateStmtCont
*** 548,553 ****
--- 548,585 ----
  				break;
  		}
  	}
+ 
+ 	/*
+ 	 * Generate ALTER FOREIGN TABLE ALTER COLUMN statement which adds 
+ 	 * per-column generic options for this column.
+ 	 */
+ 	if (column->fdwoptions != NIL)
+ 	{
+ 		AlterTableStmt *stmt;
+ 		AlterTableCmd  *cmd;
+ 
+ 		cmd = makeNode(AlterTableCmd);
+ 		cmd->subtype = AT_AlterColumnGenericOptions;
+ 		cmd->name = column->colname;
+ 		cmd->def = (Node *) column->fdwoptions;
+ 		cmd->behavior = DROP_RESTRICT;
+ 		cmd->missing_ok = false;
+ 		cmd->validated = true;
+ 
+ 		stmt = makeNode(AlterTableStmt);
+ 		stmt->relation = cxt->relation;
+ 		stmt->cmds = NIL;
+ 		stmt->relkind = OBJECT_FOREIGN_TABLE;
+ 		stmt->cmds = lappend(stmt->cmds, cmd);
+ 
+ 		cxt->alist = lappend(cxt->alist, stmt);
+ 
+ 		foreach (clist, column->fdwoptions)
+ 		{
+ 			DefElem		   *option = (DefElem *) lfirst(clist);
+ 			elog(DEBUG3, "%s=%s", option->defname, strVal(option->arg));
+ 		}
+ 	}
  }
  
  /*
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 378330b..bd1ec80 100644
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
*************** exec_command(const char *cmd,
*** 493,498 ****
--- 493,501 ----
  					case 't':
  						success = listForeignTables(pattern, show_verbose);
  						break;
+ 					case 'c':
+ 						success = listForeignTableColumns(pattern, show_verbose);
+ 						break;
  					default:
  						status = PSQL_CMD_UNKNOWN;
  						break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index b2c54b5..c0860f9 100644
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** listForeignTables(const char *pattern, b
*** 3817,3822 ****
--- 3817,3885 ----
  }
  
  /*
+  * \dec
+  *
+  * Describes foreign table columns.
+  */
+ bool
+ listForeignTableColumns(const char *pattern, bool verbose)
+ {
+ 	PQExpBufferData buf;
+ 	PGresult   *res;
+ 	printQueryOpt myopt = pset.popt;
+ 
+ 	/* FIXME should be modified to 90200 before posting patch */
+ 	if (pset.sversion < 90100)
+ 	{
+ 		fprintf(stderr, _("The server (version %d.%d) does not support foreign table column options.\n"),
+ 				pset.sversion / 10000, (pset.sversion / 100) % 100);
+ 		return true;
+ 	}
+ 
+ 	initPQExpBuffer(&buf);
+ 	printfPQExpBuffer(&buf,
+ 					  "SELECT n.nspname AS \"%s\",\n"
+ 					  "  c.relname AS \"%s\",\n"
+ 					  "  a.attname AS \"%s\"",
+ 					  gettext_noop("Schema"),
+ 					  gettext_noop("Table"),
+ 					  gettext_noop("Column"));
+ 
+ 	if (verbose)
+ 		appendPQExpBuffer(&buf,
+ 						  ",\n  a.attfdwoptions AS \"%s\"",
+ 						  gettext_noop("Options"));
+ 
+ 	appendPQExpBuffer(&buf, "\nFROM pg_catalog.pg_foreign_table ft,");
+ 	appendPQExpBuffer(&buf, "\n pg_catalog.pg_class c,");
+ 	appendPQExpBuffer(&buf, "\n pg_catalog.pg_namespace n,");
+ 	appendPQExpBuffer(&buf, "\n pg_catalog.pg_attribute a\n");
+ 	appendPQExpBuffer(&buf, "\nWHERE c.oid = ft.ftrelid");
+ 	appendPQExpBuffer(&buf, "\nAND n.oid = c.relnamespace\n");
+ 	appendPQExpBuffer(&buf, "\nAND a.attrelid= c.oid\n");
+ 	appendPQExpBuffer(&buf, "\nAND a.attnum > 0\n");
+ 
+ 	processSQLNamePattern(pset.db, &buf, pattern, true, false,
+ 						  NULL, "n.nspname", "c.relname", NULL);
+ 
+ 	appendPQExpBuffer(&buf, "ORDER BY 1, 2, a.attnum;");
+ 
+ 	res = PSQLexec(buf.data, false);
+ 	termPQExpBuffer(&buf);
+ 	if (!res)
+ 		return false;
+ 
+ 	myopt.nullPrint = NULL;
+ 	myopt.title = _("List of foreign table columns");
+ 	myopt.translate_header = true;
+ 
+ 	printQuery(res, &myopt, pset.queryFout, pset.logfile);
+ 
+ 	PQclear(res);
+ 	return true;
+ }
+ 
+ /*
   * \dx
   *
   * Briefly describes installed extensions.
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index fb86d1e..b6bc472 100644
*** a/src/bin/psql/describe.h
--- b/src/bin/psql/describe.h
*************** extern bool listUserMappings(const char 
*** 87,92 ****
--- 87,95 ----
  /* \det */
  extern bool listForeignTables(const char *pattern, bool verbose);
  
+ /* \dec */
+ extern bool listForeignTableColumns(const char *pattern, bool verbose);
+ 
  /* \dL */
  extern bool listLanguages(const char *pattern, bool verbose, bool showSystem);
  
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index ac5edca..0c8d011 100644
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
*************** slashUsage(unsigned short int pager)
*** 200,205 ****
--- 200,206 ----
  	fprintf(output, _("  \\ddp    [PATTERN]      list default privileges\n"));
  	fprintf(output, _("  \\dD[S]  [PATTERN]      list domains\n"));
  	fprintf(output, _("  \\det[+] [PATTERN]      list foreign tables\n"));
+ 	fprintf(output, _("  \\dec[+] [PATTERN]      list foreign table columns\n"));
  	fprintf(output, _("  \\des[+] [PATTERN]      list foreign servers\n"));
  	fprintf(output, _("  \\deu[+] [PATTERN]      list user mappings\n"));
  	fprintf(output, _("  \\dew[+] [PATTERN]      list foreign-data wrappers\n"));
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 0200a81..52e99a8 100644
*** a/src/include/catalog/catversion.h
--- b/src/include/catalog/catversion.h
***************
*** 53,58 ****
   */
  
  /*							yyyymmddN */
! #define CATALOG_VERSION_NO	201105231
  
  #endif
--- 53,58 ----
   */
  
  /*							yyyymmddN */
! #define CATALOG_VERSION_NO	201106141
  
  #endif
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 409d6ea..3ea87e8 100644
*** a/src/include/catalog/pg_attribute.h
--- b/src/include/catalog/pg_attribute.h
*************** CATALOG(pg_attribute,1249) BKI_BOOTSTRAP
*** 156,161 ****
--- 156,164 ----
  
  	/* Column-level options */
  	text		attoptions[1];
+ 
+ 	/* Column-level FDW options */
+ 	text		attfdwoptions[1];
  } FormData_pg_attribute;
  
  /*
*************** typedef FormData_pg_attribute *Form_pg_a
*** 179,185 ****
   * ----------------
   */
  
! #define Natts_pg_attribute				20
  #define Anum_pg_attribute_attrelid		1
  #define Anum_pg_attribute_attname		2
  #define Anum_pg_attribute_atttypid		3
--- 182,188 ----
   * ----------------
   */
  
! #define Natts_pg_attribute				21
  #define Anum_pg_attribute_attrelid		1
  #define Anum_pg_attribute_attname		2
  #define Anum_pg_attribute_atttypid		3
*************** typedef FormData_pg_attribute *Form_pg_a
*** 200,205 ****
--- 203,209 ----
  #define Anum_pg_attribute_attcollation	18
  #define Anum_pg_attribute_attacl		19
  #define Anum_pg_attribute_attoptions	20
+ #define Anum_pg_attribute_attfdwoptions	21
  
  
  /* ----------------
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index ffcce3c..caa285e 100644
*** a/src/include/catalog/pg_class.h
--- b/src/include/catalog/pg_class.h
*************** typedef FormData_pg_class *Form_pg_class
*** 132,138 ****
  /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
  DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
! DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 20 0 f f f f f 3 _null_ _null_ ));
  DESCR("");
  DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 25 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
--- 132,138 ----
  /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
  DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
! DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
  DESCR("");
  DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 25 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 14937d4..5e82220 100644
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct ColumnDef
*** 500,505 ****
--- 500,506 ----
  	CollateClause *collClause;	/* untransformed COLLATE spec, if any */
  	Oid			collOid;		/* collation OID (InvalidOid if not set) */
  	List	   *constraints;	/* other constraints on column */
+ 	List	   *fdwoptions;		/* per-column FDW options */
  } ColumnDef;
  
  /*
*************** typedef enum AlterTableType
*** 1196,1201 ****
--- 1197,1203 ----
  	AT_DropConstraint,			/* drop constraint */
  	AT_DropConstraintRecurse,	/* internal to commands/tablecmds.c */
  	AT_AlterColumnType,			/* alter column type */
+ 	AT_AlterColumnGenericOptions,	/* alter column OPTIONS (...) */
  	AT_ChangeOwner,				/* change owner */
  	AT_ClusterOn,				/* CLUSTER ON */
  	AT_DropCluster,				/* SET WITHOUT CLUSTER */
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index e18eed8..d447a2c 100644
*** a/src/test/regress/expected/foreign_data.out
--- b/src/test/regress/expected/foreign_data.out
*************** ERROR:  syntax error at or near "WITH OI
*** 646,653 ****
  LINE 1: CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;
                                                ^
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer NOT NULL,
! 	c2 text,
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
--- 646,653 ----
  LINE 1: CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;
                                                ^
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer OPTIONS (param1 'val1') NOT NULL,
! 	c2 text OPTIONS (param2 'val2', param3 'val3'),
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
*************** ALTER FOREIGN TABLE ft1 ADD COLUMN c6 in
*** 687,693 ****
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer;
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ERROR:  "ft1" is not a table or view
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
--- 687,693 ----
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1');
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ERROR:  "ft1" is not a table or view
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
*************** ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 
*** 698,703 ****
--- 698,723 ----
  ERROR:  ALTER TYPE USING is only supported on plain tables
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE text;
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN xmin OPTIONS (ADD p1 'v1'); -- ERROR
+ ERROR:  cannot alter system column "xmin"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
+                         ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ \dec+
+             List of foreign table columns
+  Schema | Table | Column |          Options          
+ --------+-------+--------+---------------------------
+  public | ft1   | c1     | {param1=val1}
+  public | ft1   | c2     | {param2=val2,param3=val3}
+  public | ft1   | c3     | 
+  public | ft1   | c4     | 
+  public | ft1   | c6     | 
+  public | ft1   | c7     | {p1=v1,p2=v2}
+  public | ft1   | c8     | {p2=V2}
+  public | ft1   | c9     | 
+  public | ft1   | c10    | {p1=v1}
+ (9 rows)
+ 
  -- can't change the column type if it's used elsewhere
  CREATE TABLE use_ft1_column_type (x ft1);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE integer;	-- ERROR
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index d323921..80d8d6b 100644
*** a/src/test/regress/sql/foreign_data.sql
--- b/src/test/regress/sql/foreign_data.sql
*************** CREATE FOREIGN TABLE ft1 () SERVER no_se
*** 264,271 ****
  CREATE FOREIGN TABLE ft1 (c1 serial) SERVER sc;                 -- ERROR
  CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;                -- ERROR
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer NOT NULL,
! 	c2 text,
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
--- 264,271 ----
  CREATE FOREIGN TABLE ft1 (c1 serial) SERVER sc;                 -- ERROR
  CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;                -- ERROR
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer OPTIONS (param1 'val1') NOT NULL,
! 	c2 text OPTIONS (param2 'val2', param3 'val3'),
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
*************** ALTER FOREIGN TABLE ft1 ADD COLUMN c6 in
*** 288,294 ****
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer;
  
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
--- 288,294 ----
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1');
  
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
*************** ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 
*** 297,302 ****
--- 297,307 ----
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10) using '0'; -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE text;
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN xmin OPTIONS (ADD p1 'v1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
+                         ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ \dec+
  -- can't change the column type if it's used elsewhere
  CREATE TABLE use_ft1_column_type (x ft1);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE integer;	-- ERROR
#2Robert Haas
robertmhaas@gmail.com
In reply to: Shigeru Hanada (#1)
Re: per-column generic option

I haven't looked at the patch yet, but here are a few comments on the
design, which overall looks good.

2011/6/14 Shigeru Hanada <shigeru.hanada@gmail.com>:

1) psql should support describing per-column generic options, so \dec
command was added.  If the form \dec+ is used, generic options are also
displayed.  Output sample is:

I would not add a new backslash command for this - it's unlikely to be
useful to see this information across all tables. It would be more
helpful to somehow (not sure of the details) incorporate this into the
output of running \d on a foreign table.

Here I found an inconsistency about privilege to see generic options
(not only column but also FDW and server et al).  The
information_schema.*_options only shows options which are associated to
objects that current user can access, but \de*+ doesn't have such
restriction.  \de* commands should be fixed to hide forbidden objects?

It's less important whether \de* is consistent with information_schema
in this regard than it is whether it is consistent with other psql
backslash commands, e.g. \dv or \db or \dC. AFAIK those commands do
not filter by privilege.

1) Is "generic options" proper term to mean FDW-specific option
associated to a FDW object?  It's used in the SQL/MED standard, but
seems not popular...  "FDW option" would be better than "generic option"?

I think FDW option is much clearer.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#3Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: Robert Haas (#2)
3 attachment(s)
Re: per-column generic option

(2011/06/14 21:20), Robert Haas wrote:

I haven't looked at the patch yet, but here are a few comments on the
design, which overall looks good.

Thanks for the review. Please find attached a revised patch.

In addition to responding to your comments, I also added pg_dump
support. Now pg_dump dumps per-column generic options with ALTER
FOREIGN TABLE ALTER COLUMN statement just after CREATE FOREIGN TABLE.
Once I've though to dump them in each column definition of a CREATE
FOREIGN TABLE statement, but that seems to makes the statement too complex.

2011/6/14 Shigeru Hanada<shigeru.hanada@gmail.com>:

1) psql should support describing per-column generic options, so \dec
command was added. If the form \dec+ is used, generic options are also
displayed. Output sample is:

I would not add a new backslash command for this - it's unlikely to be
useful to see this information across all tables. It would be more
helpful to somehow (not sure of the details) incorporate this into the
output of running \d on a foreign table.

Hm, belatedly I noticed that relation-kind-specific column are added
preceding to verbose-only columns such as expression for indexes and
column values for sequences. It seems suitable place to show per-column
generic options. Please see attached "desc_results.txt" as sample.

I also noticed that relation-kind-specific information are not mentioned
in any document (at least in the section of psql[1]http://developer.postgresql.org/pgdocs/postgres/app-psql.html), even about
existing ones such as sequence values and index definition. I also
added short brief of them to psql document.

BTW, while working around \d command, I noticed that we can avoid
variable width (# of columns) query result, which is used to fetch
column information, with using NULL as placeholder (and it has already
been used partially). I think that it would enhance maintainability
little, so I've separated this fix to another patch
avoid_variable_width_result.patch. The main patch
per_column_option_v2.patch assumes that this fix has been applied.

[1]: http://developer.postgresql.org/pgdocs/postgres/app-psql.html

Here I found an inconsistency about privilege to see generic options
(not only column but also FDW and server et al). The
information_schema.*_options only shows options which are associated to
objects that current user can access, but \de*+ doesn't have such
restriction. \de* commands should be fixed to hide forbidden objects?

It's less important whether \de* is consistent with information_schema
in this regard than it is whether it is consistent with other psql
backslash commands, e.g. \dv or \db or \dC. AFAIK those commands do
not filter by privilege.

Agreed, I'll leave \de* to show results unconditionally.

1) Is "generic options" proper term to mean FDW-specific option
associated to a FDW object? It's used in the SQL/MED standard, but
seems not popular... "FDW option" would be better than "generic option"?

I think FDW option is much clearer.

So do I, but I didn't touch them because "generic option" appears in
many documents, source files including comments and psql's \d* output.
Most of them have been there since 8.4. Is it acceptable to change them
to "FDW option", at least for only documents?

OTOH, psql's \d* commands use "Options" as column header of FDW options
and reloptions. I also left them because I thought that this would not
cause misunderstanding.

Regards,
--
Shigeru Hanada

Attachments:

desc_results.txttext/plain; name=desc_results.txtDownload
avoid_variable_width_result.patchtext/plain; name=avoid_variable_width_result.patchDownload
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index b2c54b5..7f9e1fe 100644
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** describeOneTableDetails(const char *sche
*** 1281,1287 ****
  		res = NULL;
  	}
  
! 	/* Get column info */
  	printfPQExpBuffer(&buf, "SELECT a.attname,");
  	appendPQExpBuffer(&buf, "\n  pg_catalog.format_type(a.atttypid, a.atttypmod),"
  					  "\n  (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)"
--- 1281,1292 ----
  		res = NULL;
  	}
  
! 	/*
! 	 * Get column info
! 	 *
! 	 * You need to modify value of "firstvcol" which willbe defined below if
! 	 * you are adding column(s) preceding to verbose-only columns.
! 	 */
  	printfPQExpBuffer(&buf, "SELECT a.attname,");
  	appendPQExpBuffer(&buf, "\n  pg_catalog.format_type(a.atttypid, a.atttypmod),"
  					  "\n  (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)"
*************** describeOneTableDetails(const char *sche
*** 1295,1300 ****
--- 1300,1307 ----
  		appendPQExpBuffer(&buf, "\n  NULL AS attcollation");
  	if (tableinfo.relkind == 'i')
  		appendPQExpBuffer(&buf, ",\n  pg_catalog.pg_get_indexdef(a.attrelid, a.attnum, TRUE) AS indexdef");
+ 	else
+ 		appendPQExpBuffer(&buf, ",\n  NULL AS indexdef");
  	if (verbose)
  		appendPQExpBuffer(&buf, ",\n  a.attstorage, pg_catalog.col_description(a.attrelid, a.attnum)");
  	appendPQExpBuffer(&buf, "\nFROM pg_catalog.pg_attribute a");
*************** describeOneTableDetails(const char *sche
*** 1461,1467 ****
  		/* Storage and Description */
  		if (verbose)
  		{
! 			int			firstvcol = (tableinfo.relkind == 'i' ? 7 : 6);
  			char	   *storage = PQgetvalue(res, i, firstvcol);
  
  			/* these strings are literal in our syntax, so not translated. */
--- 1468,1474 ----
  		/* Storage and Description */
  		if (verbose)
  		{
! 			int			firstvcol = 7;
  			char	   *storage = PQgetvalue(res, i, firstvcol);
  
  			/* these strings are literal in our syntax, so not translated. */
per_column_option_v2.patchtext/plain; name=per_column_option_v2.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 8504555..d1225e1 100644
*** a/doc/src/sgml/catalogs.sgml
--- b/doc/src/sgml/catalogs.sgml
***************
*** 1152,1157 ****
--- 1152,1166 ----
        </entry>
       </row>
  
+      <row>
+       <entry><structfield>attfdwoptions</structfield></entry>
+       <entry><type>text[]</type></entry>
+       <entry></entry>
+       <entry>
+        Attribute-level generic options, as <quote>keyword=value</> strings
+       </entry>
+      </row>
+ 
      </tbody>
     </tgroup>
    </table>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 6df69db..539f573 100644
*** a/doc/src/sgml/information_schema.sgml
--- b/doc/src/sgml/information_schema.sgml
***************
*** 958,963 ****
--- 958,1026 ----
    </table>
   </sect1>
  
+  <sect1 id="infoschema-column-options">
+   <title><literal>column_options</literal></title>
+ 
+   <para>
+    The view <literal>column_options</literal> contains all the
+    options defined for foreign table columns in the current database.  Only
+    those foreign table columns are shown that the current user has access to
+    (by way of being the owner or having some privilege).
+   </para>
+ 
+   <table>
+    <title><literal>column_options</literal> Columns</title>
+ 
+    <tgroup cols="3">
+     <thead>
+      <row>
+       <entry>Name</entry>
+       <entry>Data Type</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+ 
+     <tbody>
+      <row>
+       <entry><literal>table_catalog</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the database that contains the foreign table (always the current database)</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>table_schema</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the schema that contains the foreign table</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>table_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the foreign table</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>column_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the column</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>option_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of an option</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>option_value</literal></entry>
+       <entry><type>character_data</type></entry>
+       <entry>Value of the option</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect1>
+ 
   <sect1 id="infoschema-column-privileges">
    <title><literal>column_privileges</literal></title>
  
diff --git a/doc/src/sgml/ref/alter_foreign_table.sgml b/doc/src/sgml/ref/alter_foreign_table.sgml
index a45df02..95ae02a 100644
*** a/doc/src/sgml/ref/alter_foreign_table.sgml
--- b/doc/src/sgml/ref/alter_foreign_table.sgml
*************** ALTER FOREIGN TABLE <replaceable class="
*** 36,41 ****
--- 36,42 ----
      DROP [ COLUMN ] [ IF EXISTS ] <replaceable class="PARAMETER">column</replaceable> [ RESTRICT | CASCADE ]
      ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> [ SET DATA ] TYPE <replaceable class="PARAMETER">type</replaceable>
      ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> { SET | DROP } NOT NULL
+     ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
      OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
      OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
  </synopsis>
*************** ALTER FOREIGN TABLE <replaceable class="
*** 125,136 ****
      <term><literal>OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ] )</literal></term>
      <listitem>
       <para>
!       Change options for the foreign table.
        <literal>ADD</>, <literal>SET</>, and <literal>DROP</>
        specify the action to be performed.  <literal>ADD</> is assumed
!       if no operation is explicitly specified.  Option names must be
!       unique; names and values are also validated using the foreign
!       data wrapper library.
       </para>
      </listitem>
     </varlistentry>
--- 126,137 ----
      <term><literal>OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ] )</literal></term>
      <listitem>
       <para>
!       Change options for the foreign table or the column of the foreign table.
        <literal>ADD</>, <literal>SET</>, and <literal>DROP</>
        specify the action to be performed.  <literal>ADD</> is assumed
!       if no operation is explicitly specified.  Option names must be unique
!       in each associated object; names and values are also validated using the
!       foreign data wrapper library.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml
index ad91072..8863386 100644
*** a/doc/src/sgml/ref/create_foreign_table.sgml
--- b/doc/src/sgml/ref/create_foreign_table.sgml
***************
*** 19,25 ****
   <refsynopsisdiv>
  <synopsis>
  CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="PARAMETER">table_name</replaceable> ( [
!   { <replaceable class="PARAMETER">column_name</replaceable> <replaceable class="PARAMETER">data_type</replaceable> [ NULL | NOT NULL ] }
      [, ... ]
  ] )
    SERVER <replaceable class="parameter">server_name</replaceable>
--- 19,25 ----
   <refsynopsisdiv>
  <synopsis>
  CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="PARAMETER">table_name</replaceable> ( [
!   { <replaceable class="PARAMETER">column_name</replaceable> <replaceable class="PARAMETER">data_type</replaceable> [ OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ... ] ) ] [ NULL | NOT NULL ] }
      [, ... ]
  ] )
    SERVER <replaceable class="parameter">server_name</replaceable>
*************** CREATE FOREIGN TABLE [ IF NOT EXISTS ] <
*** 138,147 ****
      <term><literal>OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ...] )</literal></term>
      <listitem>
       <para>
!       Options to be associated with the new foreign table.
        The allowed option names and values are specific to each foreign
        data wrapper and are validated using the foreign-data wrapper's
!       validator function. Option names must be unique.
       </para>
      </listitem>
     </varlistentry>
--- 138,149 ----
      <term><literal>OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ...] )</literal></term>
      <listitem>
       <para>
!       Options to be associated with the new foreign table or the column of
!       the foreign table.
        The allowed option names and values are specific to each foreign
        data wrapper and are validated using the foreign-data wrapper's
!       validator function. Option names must be unique in each associated
!       object.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index c9713d3..02bc60f 100644
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** testdb=&gt;
*** 891,896 ****
--- 891,903 ----
          below.)
          </para>
  
+ 		<para>
+         In addition to common information, <literal>\d</> shows
+         relation-kind-specific information for each column:
+         column values for sequences, indexed expression for indexes and
+         per-column generic options for foreign tables.
+ 		</para>
+ 
          <para>
          The command form <literal>\d+</literal> is identical, except that
          more information is displayed: any comments associated with the
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 16979c4..4ffedab 100644
*** a/src/backend/access/common/tupdesc.c
--- b/src/backend/access/common/tupdesc.c
*************** equalTupleDescs(TupleDesc tupdesc1, Tupl
*** 362,368 ****
  			return false;
  		if (attr1->attcollation != attr2->attcollation)
  			return false;
! 		/* attacl and attoptions are not even present... */
  	}
  
  	if (tupdesc1->constr != NULL)
--- 362,368 ----
  			return false;
  		if (attr1->attcollation != attr2->attcollation)
  			return false;
! 		/* attacl, attoptions and attfdwoptions are not even present... */
  	}
  
  	if (tupdesc1->constr != NULL)
*************** TupleDescInitEntry(TupleDesc desc,
*** 482,488 ****
  	att->attisdropped = false;
  	att->attislocal = true;
  	att->attinhcount = 0;
! 	/* attacl and attoptions are not present in tupledescs */
  
  	tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(oidtypeid));
  	if (!HeapTupleIsValid(tuple))
--- 482,488 ----
  	att->attisdropped = false;
  	att->attislocal = true;
  	att->attinhcount = 0;
! 	/* attacl, attoptions and attfdwoptions are not present in tupledescs */
  
  	tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(oidtypeid));
  	if (!HeapTupleIsValid(tuple))
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index 0aeaf5b..d91af52 100644
*** a/src/backend/catalog/genbki.pl
--- b/src/backend/catalog/genbki.pl
*************** sub emit_pgattr_row
*** 369,375 ****
          attislocal    => 't',
          attinhcount   => '0',
          attacl        => '_null_',
!         attoptions    => '_null_'
      );
      return {%PGATTR_DEFAULTS, %row};
  }
--- 369,376 ----
          attislocal    => 't',
          attinhcount   => '0',
          attacl        => '_null_',
!         attoptions    => '_null_',
!         attfdwoptions => '_null_'
      );
      return {%PGATTR_DEFAULTS, %row};
  }
*************** sub emit_schemapg_row
*** 400,405 ****
--- 401,407 ----
      # Only the fixed-size portions of the descriptors are ever used.
      delete $row->{attacl};
      delete $row->{attoptions};
+     delete $row->{attfdwoptions};
  
      # Expand booleans from 'f'/'t' to 'false'/'true'.
      # Some values might be other macros (eg FLOAT4PASSBYVAL), don't change.
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index a6e541d..f3090e2 100644
*** a/src/backend/catalog/heap.c
--- b/src/backend/catalog/heap.c
*************** static List *insert_ordered_unique_oid(L
*** 126,132 ****
   */
  
  /*
!  * The initializers below do not include the attoptions or attacl fields,
   * but that's OK - we're never going to reference anything beyond the
   * fixed-size portion of the structure anyway.
   */
--- 126,132 ----
   */
  
  /*
!  * The initializers below do not include trailing variable length fields,
   * but that's OK - we're never going to reference anything beyond the
   * fixed-size portion of the structure anyway.
   */
*************** InsertPgAttributeTuple(Relation pg_attri
*** 607,612 ****
--- 607,614 ----
  	/* start out with empty permissions and empty options */
  	nulls[Anum_pg_attribute_attacl - 1] = true;
  	nulls[Anum_pg_attribute_attoptions - 1] = true;
+ 	/* FIXME use content of OPTIONS (...) if any. */
+ 	nulls[Anum_pg_attribute_attfdwoptions - 1] = true;
  
  	tup = heap_form_tuple(RelationGetDescr(pg_attribute_rel), values, nulls);
  
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 81407a3..407b3ca 100644
*** a/src/backend/catalog/information_schema.sql
--- b/src/backend/catalog/information_schema.sql
*************** GRANT SELECT ON element_types TO PUBLIC;
*** 2449,2454 ****
--- 2449,2487 ----
  
  -- SQL/MED views; these use section numbers from part 9 of the standard.
  
+ /* Base view for foreign table columns */
+ CREATE VIEW _pg_foreign_table_columns AS
+     SELECT n.nspname,
+            c.relname,
+            a.attname,
+            a.attfdwoptions
+     FROM pg_foreign_table t, pg_authid u, pg_namespace n, pg_class c,
+          pg_attribute a
+     WHERE u.oid = c.relowner
+           AND (pg_has_role(c.relowner, 'USAGE')
+                OR has_column_privilege(c.oid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'))
+           AND n.oid = c.relnamespace
+           AND c.oid = t.ftrelid
+           AND c.relkind = 'f'
+           AND a.attrelid = c.oid
+           AND a.attnum > 0;
+ 
+ /*
+  * 24.2
+  * COLUMN_OPTIONS view
+  */
+ CREATE VIEW column_options AS
+     SELECT CAST(current_database() AS sql_identifier) AS table_catalog,
+            c.nspname AS table_schema,
+            c.relname AS table_name,
+            c.attname AS column_name,
+            CAST((pg_options_to_table(c.attfdwoptions)).option_name AS sql_identifier) AS option_name,
+            CAST((pg_options_to_table(c.attfdwoptions)).option_value AS character_data) AS option_value
+     FROM _pg_foreign_table_columns c;
+ 
+ GRANT SELECT ON column_options TO PUBLIC;
+ 
+ 
  /* Base view for foreign-data wrappers */
  CREATE VIEW _pg_foreign_data_wrappers AS
      SELECT w.oid,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2c9f855..d604273 100644
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** static void ATPrepAlterColumnType(List *
*** 343,348 ****
--- 343,349 ----
  static bool ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno);
  static void ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
  					  AlterTableCmd *cmd, LOCKMODE lockmode);
+ static void ATExecAlterColumnGenericOptions(Relation rel, const char *colName, List *options, LOCKMODE lockmode);
  static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode);
  static void ATPostAlterTypeParse(char *cmd, List **wqueue, LOCKMODE lockmode);
  static void change_owner_recurse_to_sequences(Oid relationOid,
*************** AlterTableGetLockLevel(List *cmds)
*** 2606,2611 ****
--- 2607,2613 ----
  			case AT_DropNotNull:		/* may change some SQL plans */
  			case AT_SetNotNull:
  			case AT_GenericOptions:
+ 			case AT_AlterColumnGenericOptions:
  				cmd_lockmode = AccessExclusiveLock;
  				break;
  
*************** ATPrepCmd(List **wqueue, Relation rel, A
*** 2880,2885 ****
--- 2882,2893 ----
  			ATPrepAlterColumnType(wqueue, tab, rel, recurse, recursing, cmd, lockmode);
  			pass = AT_PASS_ALTER_TYPE;
  			break;
+ 		case AT_AlterColumnGenericOptions:
+ 			ATSimplePermissions(rel, ATT_FOREIGN_TABLE);
+ 			/* This command never recurses */
+ 			/* No command-specific prep needed */
+ 			pass = AT_PASS_MISC;
+ 			break;
  		case AT_ChangeOwner:	/* ALTER OWNER */
  			/* This command never recurses */
  			/* No command-specific prep needed */
*************** ATExecCmd(List **wqueue, AlteredTableInf
*** 3113,3118 ****
--- 3121,3129 ----
  		case AT_AlterColumnType:		/* ALTER COLUMN TYPE */
  			ATExecAlterColumnType(tab, rel, cmd, lockmode);
  			break;
+ 		case AT_AlterColumnGenericOptions:	/* ALTER COLUMN OPTIONS */
+ 			ATExecAlterColumnGenericOptions(rel, cmd->name, (List *) cmd->def, lockmode);
+ 			break;
  		case AT_ChangeOwner:	/* ALTER OWNER */
  			ATExecChangeOwner(RelationGetRelid(rel),
  							  get_role_oid(cmd->name, false),
*************** ATExecAlterColumnType(AlteredTableInfo *
*** 7162,7167 ****
--- 7173,7272 ----
  	heap_freetuple(heapTup);
  }
  
+ static void
+ ATExecAlterColumnGenericOptions(Relation rel,
+ 								const char *colName,
+ 								List *options,
+ 								LOCKMODE lockmode)
+ {
+ 	Relation	ftrel;
+ 	Relation	attrel;
+ 	ForeignServer *server;
+ 	ForeignDataWrapper *fdw;
+ 	HeapTuple	tuple;
+ 	HeapTuple	newtuple;
+ 	bool		isnull;
+ 	Datum		repl_val[Natts_pg_attribute];
+ 	bool		repl_null[Natts_pg_attribute];
+ 	bool		repl_repl[Natts_pg_attribute];
+ 	Datum		datum;
+ 	Form_pg_foreign_table fttableform;
+ 	Form_pg_attribute atttableform;
+ 
+ 	if (options == NIL)
+ 		return;
+ 
+ 	/* First, determine FDW validator associated to the foreign table. */
+ 	ftrel = heap_open(ForeignTableRelationId, AccessShareLock);
+ 	tuple = SearchSysCache1(FOREIGNTABLEREL, rel->rd_id);
+ 	if (!HeapTupleIsValid(tuple))
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_UNDEFINED_OBJECT),
+ 				 errmsg("foreign table \"%s\" does not exist",
+ 						RelationGetRelationName(rel))));
+ 	fttableform = (Form_pg_foreign_table) GETSTRUCT(tuple);
+ 	server = GetForeignServer(fttableform->ftserver);
+ 	fdw = GetForeignDataWrapper(server->fdwid);
+ 
+ 	heap_close(ftrel, AccessShareLock);
+ 	ReleaseSysCache(tuple);
+ 
+ 	attrel = heap_open(AttributeRelationId, RowExclusiveLock);
+ 	tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName);
+ 	if (!HeapTupleIsValid(tuple))
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_UNDEFINED_COLUMN),
+ 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+ 						colName, RelationGetRelationName(rel))));
+ 
+ 	/* Prevent them from altering a system attribute */
+ 	atttableform = (Form_pg_attribute) GETSTRUCT(tuple);
+ 	if (atttableform->attnum <= 0)
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ 				 errmsg("cannot alter system column \"%s\"", colName)));
+ 
+ 
+ 	/* Initialize buffers for new tuple values */
+ 	memset(repl_val, 0, sizeof(repl_val));
+ 	memset(repl_null, false, sizeof(repl_null));
+ 	memset(repl_repl, false, sizeof(repl_repl));
+ 
+ 	/* Extract the current options */
+ 	datum = SysCacheGetAttr(ATTNAME,
+ 							tuple,
+ 							Anum_pg_attribute_attfdwoptions,
+ 							&isnull);
+ 	if (isnull)
+ 		datum = PointerGetDatum(NULL);
+ 
+ 	/* Transform the options */
+ 	datum = transformGenericOptions(AttributeRelationId,
+ 									datum,
+ 									options,
+ 									fdw->fdwvalidator);
+ 
+ 	if (PointerIsValid(DatumGetPointer(datum)))
+ 		repl_val[Anum_pg_attribute_attfdwoptions - 1] = datum;
+ 	else
+ 		repl_null[Anum_pg_attribute_attfdwoptions - 1] = true;
+ 
+ 	repl_repl[Anum_pg_attribute_attfdwoptions - 1] = true;
+ 
+ 	/* Everything looks good - update the tuple */
+ 
+ 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrel),
+ 								 repl_val, repl_null, repl_repl);
+ 	ReleaseSysCache(tuple);
+ 
+ 	simple_heap_update(attrel, &newtuple->t_self, newtuple);
+ 	CatalogUpdateIndexes(attrel, newtuple);
+ 
+ 	heap_close(attrel, RowExclusiveLock);
+ 
+ 	heap_freetuple(newtuple);
+ }
+ 
  /*
   * Cleanup after we've finished all the ALTER TYPE operations for a
   * particular relation.  We have to drop and recreate all the indexes
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c9133dd..148110f 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyColumnDef(ColumnDef *from)
*** 2312,2317 ****
--- 2312,2318 ----
  	COPY_NODE_FIELD(collClause);
  	COPY_SCALAR_FIELD(collOid);
  	COPY_NODE_FIELD(constraints);
+ 	COPY_NODE_FIELD(fdwoptions);
  
  	return newnode;
  }
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 681f5f8..cb9be13 100644
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outColumnDef(StringInfo str, ColumnDef 
*** 2101,2106 ****
--- 2101,2107 ----
  	WRITE_NODE_FIELD(collClause);
  	WRITE_OID_FIELD(collOid);
  	WRITE_NODE_FIELD(constraints);
+ 	WRITE_NODE_FIELD(fdwoptions);
  }
  
  static void
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 1d39674..c6c3c39 100644
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** alter_table_cmd:
*** 1760,1765 ****
--- 1760,1774 ----
  					def->raw_default = $8;
  					$$ = (Node *)n;
  				}
+ 			/* ALTER FOREIGN TABLE <name> ALTER [COLUMN] <colname> OPTIONS */
+ 			| ALTER opt_column ColId alter_generic_options
+ 				{
+ 					AlterTableCmd *n = makeNode(AlterTableCmd);
+ 					n->subtype = AT_AlterColumnGenericOptions;
+ 					n->name = $3;
+ 					n->def = (Node *) $4;
+ 					$$ = (Node *)n;
+ 				}
  			/* ALTER TABLE <name> ADD CONSTRAINT ... */
  			| ADD_P TableConstraint
  				{
*************** TypedTableElement:
*** 2488,2494 ****
  			| TableConstraint					{ $$ = $1; }
  		;
  
! columnDef:	ColId Typename ColQualList
  				{
  					ColumnDef *n = makeNode(ColumnDef);
  					n->colname = $1;
--- 2497,2503 ----
  			| TableConstraint					{ $$ = $1; }
  		;
  
! columnDef:	ColId Typename create_generic_options ColQualList
  				{
  					ColumnDef *n = makeNode(ColumnDef);
  					n->colname = $1;
*************** columnDef:	ColId Typename ColQualList
*** 2501,2507 ****
  					n->raw_default = NULL;
  					n->cooked_default = NULL;
  					n->collOid = InvalidOid;
! 					SplitColQualList($3, &n->constraints, &n->collClause,
  									 yyscanner);
  					$$ = (Node *)n;
  				}
--- 2510,2517 ----
  					n->raw_default = NULL;
  					n->cooked_default = NULL;
  					n->collOid = InvalidOid;
! 					n->fdwoptions = $3;
! 					SplitColQualList($4, &n->constraints, &n->collClause,
  									 yyscanner);
  					$$ = (Node *)n;
  				}
*************** AlterFdwStmt: ALTER FOREIGN DATA_P WRAPP
*** 3681,3687 ****
  /* Options definition for CREATE FDW, SERVER and USER MAPPING */
  create_generic_options:
  			OPTIONS '(' generic_option_list ')'			{ $$ = $3; }
! 			| /*EMPTY*/									{ $$ = NIL; }
  		;
  
  generic_option_list:
--- 3691,3697 ----
  /* Options definition for CREATE FDW, SERVER and USER MAPPING */
  create_generic_options:
  			OPTIONS '(' generic_option_list ')'			{ $$ = $3; }
! 			| /*EMPTY*/									{ $$ = NIL }
  		;
  
  generic_option_list:
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 485bf4b..69d3e72 100644
*** a/src/backend/parser/parse_utilcmd.c
--- b/src/backend/parser/parse_utilcmd.c
*************** transformColumnDefinition(CreateStmtCont
*** 548,553 ****
--- 548,585 ----
  				break;
  		}
  	}
+ 
+ 	/*
+ 	 * Generate ALTER FOREIGN TABLE ALTER COLUMN statement which adds 
+ 	 * per-column generic options for this column.
+ 	 */
+ 	if (column->fdwoptions != NIL)
+ 	{
+ 		AlterTableStmt *stmt;
+ 		AlterTableCmd  *cmd;
+ 
+ 		cmd = makeNode(AlterTableCmd);
+ 		cmd->subtype = AT_AlterColumnGenericOptions;
+ 		cmd->name = column->colname;
+ 		cmd->def = (Node *) column->fdwoptions;
+ 		cmd->behavior = DROP_RESTRICT;
+ 		cmd->missing_ok = false;
+ 		cmd->validated = true;
+ 
+ 		stmt = makeNode(AlterTableStmt);
+ 		stmt->relation = cxt->relation;
+ 		stmt->cmds = NIL;
+ 		stmt->relkind = OBJECT_FOREIGN_TABLE;
+ 		stmt->cmds = lappend(stmt->cmds, cmd);
+ 
+ 		cxt->alist = lappend(cxt->alist, stmt);
+ 
+ 		foreach (clist, column->fdwoptions)
+ 		{
+ 			DefElem		   *option = (DefElem *) lfirst(clist);
+ 			elog(DEBUG3, "%s=%s", option->defname, strVal(option->arg));
+ 		}
+ 	}
  }
  
  /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 9e69b0f..04ca7f6 100644
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5556,5561 ****
--- 5556,5562 ----
  	int			i_attislocal;
  	int			i_attoptions;
  	int			i_attcollation;
+ 	int			i_attfdwoptions;
  	PGresult   *res;
  	int			ntups;
  	bool		hasdefaults;
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5593,5599 ****
  
  		resetPQExpBuffer(q);
  
! 		if (g_fout->remoteVersion >= 90100)
  		{
  			/*
  			 * attcollation is new in 9.1.	Since we only want to dump COLLATE
--- 5594,5624 ----
  
  		resetPQExpBuffer(q);
  
! 		if (g_fout->remoteVersion >= 90200)
! 		{
! 			/*
! 			 * attfdwoptions is new in 9.2.
! 			 */
! 			appendPQExpBuffer(q, "SELECT a.attnum, a.attname, a.atttypmod, "
! 							  "a.attstattarget, a.attstorage, t.typstorage, "
! 							  "a.attnotnull, a.atthasdef, a.attisdropped, "
! 							  "a.attlen, a.attalign, a.attislocal, "
! 				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
! 						"array_to_string(a.attoptions, ', ') AS attoptions, "
! 							  "CASE WHEN a.attcollation <> t.typcollation "
! 							"THEN a.attcollation ELSE 0 END AS attcollation, "
! 				  "array_to_string(ARRAY("
! 				  "  SELECT option_name || ' ' || quote_literal(option_value) "
! 				  "  FROM pg_options_to_table(attfdwoptions)), ', ') "
! 				  " AS attfdwoptions "
! 			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
! 							  "ON a.atttypid = t.oid "
! 							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
! 							  "AND a.attnum > 0::pg_catalog.int2 "
! 							  "ORDER BY a.attrelid, a.attnum",
! 							  tbinfo->dobj.catId.oid);
! 		}
! 		else if (g_fout->remoteVersion >= 90100)
  		{
  			/*
  			 * attcollation is new in 9.1.	Since we only want to dump COLLATE
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5616,5621 ****
--- 5641,5647 ----
  							  "ORDER BY a.attrelid, a.attnum",
  							  tbinfo->dobj.catId.oid);
  		}
+ 
  		else if (g_fout->remoteVersion >= 90000)
  		{
  			/* attoptions is new in 9.0 */
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5708,5713 ****
--- 5734,5740 ----
  		i_attislocal = PQfnumber(res, "attislocal");
  		i_attoptions = PQfnumber(res, "attoptions");
  		i_attcollation = PQfnumber(res, "attcollation");
+ 		i_attfdwoptions = PQfnumber(res, "attfdwoptions");
  
  		tbinfo->numatts = ntups;
  		tbinfo->attnames = (char **) malloc(ntups * sizeof(char *));
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5724,5729 ****
--- 5751,5757 ----
  		tbinfo->attrdefs = (AttrDefInfo **) malloc(ntups * sizeof(AttrDefInfo *));
  		tbinfo->attoptions = (char **) malloc(ntups * sizeof(char *));
  		tbinfo->attcollation = (Oid *) malloc(ntups * sizeof(Oid));
+ 		tbinfo->attfdwoptions = (char **) malloc(ntups * sizeof(char *));
  		tbinfo->inhAttrs = (bool *) malloc(ntups * sizeof(bool));
  		tbinfo->inhAttrDef = (bool *) malloc(ntups * sizeof(bool));
  		tbinfo->inhNotNull = (bool *) malloc(ntups * sizeof(bool));
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5750,5755 ****
--- 5778,5784 ----
  			tbinfo->notnull[j] = (PQgetvalue(res, j, i_attnotnull)[0] == 't');
  			tbinfo->attoptions[j] = strdup(PQgetvalue(res, j, i_attoptions));
  			tbinfo->attcollation[j] = atooid(PQgetvalue(res, j, i_attcollation));
+ 			tbinfo->attfdwoptions[j] = strdup(PQgetvalue(res, j, i_attfdwoptions));
  			tbinfo->attrdefs[j] = NULL; /* fix below */
  			if (PQgetvalue(res, j, i_atthasdef)[0] == 't')
  				hasdefaults = true;
*************** dumpTableSchema(Archive *fout, TableInfo
*** 12451,12456 ****
--- 12480,12500 ----
  				appendPQExpBuffer(q, "SET (%s);\n",
  								  tbinfo->attoptions[j]);
  			}
+ 
+ 			/*
+ 			 * Dump per-column generic options.
+ 			 */
+ 			if (tbinfo->relkind == RELKIND_FOREIGN_TABLE &&
+ 				tbinfo->attfdwoptions[j] &&
+ 				tbinfo->attfdwoptions[j][0] != '\0')
+ 			{
+ 				appendPQExpBuffer(q, "ALTER FOREIGN TABLE %s ",
+ 								  fmtId(tbinfo->dobj.name));
+ 				appendPQExpBuffer(q, "ALTER COLUMN %s ",
+ 								  fmtId(tbinfo->attnames[j]));
+ 				appendPQExpBuffer(q, "OPTIONS (%s);\n",
+ 								  tbinfo->attfdwoptions[j]);
+ 			}
  		}
  	}
  
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index c95614b..0a65401 100644
*** a/src/bin/pg_dump/pg_dump.h
--- b/src/bin/pg_dump/pg_dump.h
*************** typedef struct _tableInfo
*** 275,280 ****
--- 275,281 ----
  	bool	   *attislocal;		/* true if attr has local definition */
  	char	  **attoptions;		/* per-attribute options */
  	Oid		   *attcollation;	/* per-attribute collation selection */
+ 	char	  **attfdwoptions;	/* per-attribute generic options */
  
  	/*
  	 * Note: we need to store per-attribute notnull, default, and constraint
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 7f9e1fe..381150c 100644
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** describeOneTableDetails(const char *sche
*** 1302,1307 ****
--- 1302,1311 ----
  		appendPQExpBuffer(&buf, ",\n  pg_catalog.pg_get_indexdef(a.attrelid, a.attnum, TRUE) AS indexdef");
  	else
  		appendPQExpBuffer(&buf, ",\n  NULL AS indexdef");
+ 	if (tableinfo.relkind == 'f' && pset.sversion >= 90200)
+ 		appendPQExpBuffer(&buf, ",\n  a.attfdwoptions");
+ 	else
+ 		appendPQExpBuffer(&buf, ",\n  NULL AS attfdwoptions");
  	if (verbose)
  		appendPQExpBuffer(&buf, ",\n  a.attstorage, pg_catalog.col_description(a.attrelid, a.attnum)");
  	appendPQExpBuffer(&buf, "\nFROM pg_catalog.pg_attribute a");
*************** describeOneTableDetails(const char *sche
*** 1383,1388 ****
--- 1387,1395 ----
  	if (tableinfo.relkind == 'i')
  		headers[cols++] = gettext_noop("Definition");
  
+ 	if (tableinfo.relkind == 'f' && pset.sversion >= 90200)
+ 		headers[cols++] = gettext_noop("Options");
+ 
  	if (verbose)
  	{
  		headers[cols++] = gettext_noop("Storage");
*************** describeOneTableDetails(const char *sche
*** 1465,1474 ****
  		if (tableinfo.relkind == 'i')
  			printTableAddCell(&cont, PQgetvalue(res, i, 6), false, false);
  
  		/* Storage and Description */
  		if (verbose)
  		{
! 			int			firstvcol = 7;
  			char	   *storage = PQgetvalue(res, i, firstvcol);
  
  			/* these strings are literal in our syntax, so not translated. */
--- 1472,1485 ----
  		if (tableinfo.relkind == 'i')
  			printTableAddCell(&cont, PQgetvalue(res, i, 6), false, false);
  
+ 		/* FDW options for foreign table column, only for 9.2 or later */
+ 		if (tableinfo.relkind == 'f' && pset.sversion >= 90200)
+ 			printTableAddCell(&cont, PQgetvalue(res, i, 7), false, false);
+ 
  		/* Storage and Description */
  		if (verbose)
  		{
! 			int			firstvcol = 8;
  			char	   *storage = PQgetvalue(res, i, firstvcol);
  
  			/* these strings are literal in our syntax, so not translated. */
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index fb86d1e..b6bc472 100644
*** a/src/bin/psql/describe.h
--- b/src/bin/psql/describe.h
*************** extern bool listUserMappings(const char 
*** 87,92 ****
--- 87,95 ----
  /* \det */
  extern bool listForeignTables(const char *pattern, bool verbose);
  
+ /* \dec */
+ extern bool listForeignTableColumns(const char *pattern, bool verbose);
+ 
  /* \dL */
  extern bool listLanguages(const char *pattern, bool verbose, bool showSystem);
  
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 0200a81..88a4654 100644
*** a/src/include/catalog/catversion.h
--- b/src/include/catalog/catversion.h
***************
*** 53,58 ****
   */
  
  /*							yyyymmddN */
! #define CATALOG_VERSION_NO	201105231
  
  #endif
--- 53,58 ----
   */
  
  /*							yyyymmddN */
! #define CATALOG_VERSION_NO	201106151
  
  #endif
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 409d6ea..3ea87e8 100644
*** a/src/include/catalog/pg_attribute.h
--- b/src/include/catalog/pg_attribute.h
*************** CATALOG(pg_attribute,1249) BKI_BOOTSTRAP
*** 156,161 ****
--- 156,164 ----
  
  	/* Column-level options */
  	text		attoptions[1];
+ 
+ 	/* Column-level FDW options */
+ 	text		attfdwoptions[1];
  } FormData_pg_attribute;
  
  /*
*************** typedef FormData_pg_attribute *Form_pg_a
*** 179,185 ****
   * ----------------
   */
  
! #define Natts_pg_attribute				20
  #define Anum_pg_attribute_attrelid		1
  #define Anum_pg_attribute_attname		2
  #define Anum_pg_attribute_atttypid		3
--- 182,188 ----
   * ----------------
   */
  
! #define Natts_pg_attribute				21
  #define Anum_pg_attribute_attrelid		1
  #define Anum_pg_attribute_attname		2
  #define Anum_pg_attribute_atttypid		3
*************** typedef FormData_pg_attribute *Form_pg_a
*** 200,205 ****
--- 203,209 ----
  #define Anum_pg_attribute_attcollation	18
  #define Anum_pg_attribute_attacl		19
  #define Anum_pg_attribute_attoptions	20
+ #define Anum_pg_attribute_attfdwoptions	21
  
  
  /* ----------------
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index ffcce3c..caa285e 100644
*** a/src/include/catalog/pg_class.h
--- b/src/include/catalog/pg_class.h
*************** typedef FormData_pg_class *Form_pg_class
*** 132,138 ****
  /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
  DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
! DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 20 0 f f f f f 3 _null_ _null_ ));
  DESCR("");
  DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 25 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
--- 132,138 ----
  /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
  DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
! DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
  DESCR("");
  DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 25 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 14937d4..5e82220 100644
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct ColumnDef
*** 500,505 ****
--- 500,506 ----
  	CollateClause *collClause;	/* untransformed COLLATE spec, if any */
  	Oid			collOid;		/* collation OID (InvalidOid if not set) */
  	List	   *constraints;	/* other constraints on column */
+ 	List	   *fdwoptions;		/* per-column FDW options */
  } ColumnDef;
  
  /*
*************** typedef enum AlterTableType
*** 1196,1201 ****
--- 1197,1203 ----
  	AT_DropConstraint,			/* drop constraint */
  	AT_DropConstraintRecurse,	/* internal to commands/tablecmds.c */
  	AT_AlterColumnType,			/* alter column type */
+ 	AT_AlterColumnGenericOptions,	/* alter column OPTIONS (...) */
  	AT_ChangeOwner,				/* change owner */
  	AT_ClusterOn,				/* CLUSTER ON */
  	AT_DropCluster,				/* SET WITHOUT CLUSTER */
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index e18eed8..60522fe 100644
*** a/src/test/regress/expected/foreign_data.out
--- b/src/test/regress/expected/foreign_data.out
*************** ERROR:  syntax error at or near "WITH OI
*** 646,664 ****
  LINE 1: CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;
                                                ^
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer NOT NULL,
! 	c2 text,
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
  COMMENT ON COLUMN ft1.c1 IS 'ft1.c1';
  \d+ ft1
!               Foreign table "public.ft1"
!  Column |  Type   | Modifiers | Storage  | Description 
! --------+---------+-----------+----------+-------------
!  c1     | integer | not null  | plain    | ft1.c1
!  c2     | text    |           | extended | 
!  c3     | date    |           | plain    | 
  Server: sc
  Has OIDs: no
  
--- 646,664 ----
  LINE 1: CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;
                                                ^
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer OPTIONS (param1 'val1') NOT NULL,
! 	c2 text OPTIONS (param2 'val2', param3 'val3'),
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
  COMMENT ON COLUMN ft1.c1 IS 'ft1.c1';
  \d+ ft1
!                             Foreign table "public.ft1"
!  Column |  Type   | Modifiers |          Options          | Storage  | Description 
! --------+---------+-----------+---------------------------+----------+-------------
!  c1     | integer | not null  | {param1=val1}             | plain    | ft1.c1
!  c2     | text    |           | {param2=val2,param3=val3} | extended | 
!  c3     | date    |           |                           | plain    | 
  Server: sc
  Has OIDs: no
  
*************** ALTER FOREIGN TABLE ft1 ADD COLUMN c6 in
*** 687,693 ****
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer;
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ERROR:  "ft1" is not a table or view
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
--- 687,693 ----
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1');
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ERROR:  "ft1" is not a table or view
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
*************** ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 
*** 698,703 ****
--- 698,724 ----
  ERROR:  ALTER TYPE USING is only supported on plain tables
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE text;
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN xmin OPTIONS (ADD p1 'v1'); -- ERROR
+ ERROR:  cannot alter system column "xmin"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
+                         ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ \d+ ft1
+                             Foreign table "public.ft1"
+  Column |  Type   | Modifiers |          Options          | Storage  | Description 
+ --------+---------+-----------+---------------------------+----------+-------------
+  c1     | integer | not null  | {param1=val1}             | plain    | 
+  c2     | text    |           | {param2=val2,param3=val3} | extended | 
+  c3     | date    |           |                           | plain    | 
+  c4     | integer |           |                           | plain    | 
+  c6     | integer | not null  |                           | plain    | 
+  c7     | integer |           | {p1=v1,p2=v2}             | plain    | 
+  c8     | text    |           | {p2=V2}                   | extended | 
+  c9     | integer |           |                           | plain    | 
+  c10    | integer |           | {p1=v1}                   | plain    | 
+ Server: sc
+ Has OIDs: no
+ 
  -- can't change the column type if it's used elsewhere
  CREATE TABLE use_ft1_column_type (x ft1);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE integer;	-- ERROR
*************** ERROR:  relation "ft1" does not exist
*** 726,742 ****
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME c1 TO foreign_column_1;
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME TO foreign_table_1;
  \d foreign_schema.foreign_table_1
! Foreign table "foreign_schema.foreign_table_1"
!       Column      |  Type   | Modifiers 
! ------------------+---------+-----------
!  foreign_column_1 | integer | not null
!  c2               | text    | 
!  c3               | date    | 
!  c4               | integer | 
!  c6               | integer | not null
!  c7               | integer | 
!  c8               | text    | 
!  c10              | integer | 
  Server: sc
  
  -- Information schema
--- 747,763 ----
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME c1 TO foreign_column_1;
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME TO foreign_table_1;
  \d foreign_schema.foreign_table_1
!            Foreign table "foreign_schema.foreign_table_1"
!       Column      |  Type   | Modifiers |          Options          
! ------------------+---------+-----------+---------------------------
!  foreign_column_1 | integer | not null  | {param1=val1}
!  c2               | text    |           | {param2=val2,param3=val3}
!  c3               | date    |           | 
!  c4               | integer |           | 
!  c6               | integer | not null  | 
!  c7               | integer |           | {p1=v1,p2=v2}
!  c8               | text    |           | {p2=V2}
!  c10              | integer |           | {p1=v1}
  Server: sc
  
  -- Information schema
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index d323921..5d9b24a 100644
*** a/src/test/regress/sql/foreign_data.sql
--- b/src/test/regress/sql/foreign_data.sql
*************** CREATE FOREIGN TABLE ft1 () SERVER no_se
*** 264,271 ****
  CREATE FOREIGN TABLE ft1 (c1 serial) SERVER sc;                 -- ERROR
  CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;                -- ERROR
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer NOT NULL,
! 	c2 text,
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
--- 264,271 ----
  CREATE FOREIGN TABLE ft1 (c1 serial) SERVER sc;                 -- ERROR
  CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;                -- ERROR
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer OPTIONS (param1 'val1') NOT NULL,
! 	c2 text OPTIONS (param2 'val2', param3 'val3'),
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
*************** ALTER FOREIGN TABLE ft1 ADD COLUMN c6 in
*** 288,294 ****
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer;
  
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
--- 288,294 ----
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1');
  
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
*************** ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 
*** 297,302 ****
--- 297,307 ----
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10) using '0'; -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE text;
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN xmin OPTIONS (ADD p1 'v1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
+                         ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ \d+ ft1
  -- can't change the column type if it's used elsewhere
  CREATE TABLE use_ft1_column_type (x ft1);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE integer;	-- ERROR
#4David Fetter
david@fetter.org
In reply to: Shigeru Hanada (#1)
Re: per-column generic option

On Tue, Jun 14, 2011 at 05:56:05PM +0900, Shigeru Hanada wrote:

Hi,

I would like to propose support for per-column generic option, which
is defined in the SQL/MED standard. In 9.0 release, support for
foreign tables and per-table generic option have been added, but
support for per-column generic option hasn't.

Please examine the description below and attached patch
per_column_option_v1.patch. Any comments or questions are welcome.

Sorry not to respond sooner.

First, the per-column generic options are a great thing for us to
have. :)

I have an idea I've been using for the next release of DBI-Link that
has varying levels of data type mapping. In general, these mappings
would be units of executable code, one in-bound, and one out-bound,
for each of:

Universe (everything, default "mapping" is the identity map, i.e. a no-op)
Database type (e.g. MySQL)
Instance (e.g. mysql://foo.bar.com:5432)
Database
Schema
Table
Column

I didn't include row in the hierarchy because I couldn't think of a
way to identify rows across DBMSs and stable over time.

The finest-grain transformation that's been set would be the one
actually used.

Here's an example of a non-trivial mapping.

Database type:
MySQL
Foreign data type:
datetime
PostgreSQL data type:
timestamptz
Transformation direction:
Import
Transformation:
CASE
WHEN DATA = '0000-00-00 00:00:00'
THEN NULL
ELSE DATA
END

Here, I'm making the simplifying assumption that there is a bijective
mapping between data types.

Is there some way to fit the per-column part of such a mapping into
this scheme? We'd need to do some dependency tracking in order to be
able to point to the appropriate code...

Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#5Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: David Fetter (#4)
Re: per-column generic option

(2011/06/17 8:44), David Fetter wrote:

Sorry not to respond sooner.

First, the per-column generic options are a great thing for us to
have. :)

Thanks for the comments. :-)

I have an idea I've been using for the next release of DBI-Link that
has varying levels of data type mapping. In general, these mappings
would be units of executable code, one in-bound, and one out-bound,
for each of:

Universe (everything, default "mapping" is the identity map, i.e. a no-op)
Database type (e.g. MySQL)
Instance (e.g. mysql://foo.bar.com:5432)
Database
Schema
Table
Column

Some of them seem to be able to be mapped to FDW object, e.g. Database
to SERVER and Table to FOREIGN TABLE.

I didn't include row in the hierarchy because I couldn't think of a
way to identify rows across DBMSs and stable over time.

The finest-grain transformation that's been set would be the one
actually used.

Here's an example of a non-trivial mapping.

Database type:
MySQL
Foreign data type:
datetime
PostgreSQL data type:
timestamptz
Transformation direction:
Import
Transformation:
CASE
WHEN DATA = '0000-00-00 00:00:00'
THEN NULL
ELSE DATA
END

Here, I'm making the simplifying assumption that there is a bijective
mapping between data types.

Is there some way to fit the per-column part of such a mapping into
this scheme? We'd need to do some dependency tracking in order to be
able to point to the appropriate code...

IIUC, you are talking about using FDW options as storage of data type
mapping setting, or mapping definition itself, right? If so, a foreign
table needs to be created to use per-column FDW options. Does it suit
to your idea?

BTW, I couldn't get what you mean by "dependency tracking". You mean
the dependency between foreign column and local column? It might
include essence of your idea... Would you explain the detail?

Regards,
--
Shigeru Hanada

#6David Fetter
david@fetter.org
In reply to: Shigeru Hanada (#5)
Re: per-column generic option

On Fri, Jun 17, 2011 at 07:19:39PM +0900, Shigeru Hanada wrote:

(2011/06/17 8:44), David Fetter wrote:

Sorry not to respond sooner.

First, the per-column generic options are a great thing for us to
have. :)

Thanks for the comments. :-)

I have an idea I've been using for the next release of DBI-Link that
has varying levels of data type mapping. In general, these mappings
would be units of executable code, one in-bound, and one out-bound,
for each of:

Universe (everything, default "mapping" is the identity map, i.e. a no-op)
Database type (e.g. MySQL)
Instance (e.g. mysql://foo.bar.com:5432)
Database
Schema
Table
Column

Some of them seem to be able to be mapped to FDW object, e.g. Database
to SERVER and Table to FOREIGN TABLE.

Yes, I see there are a few missing. "Universe" doesn't really need
much of anything, as far as I can tell, except if we wanted to do
something that affected SQL/MED globally. Is that hierarchy otherwise
OK? DB2 may have one more level between Instance and Database Type,
that latter being the province of an individual FDW.

I didn't include row in the hierarchy because I couldn't think of a
way to identify rows across DBMSs and stable over time.

The finest-grain transformation that's been set would be the one
actually used.

Here's an example of a non-trivial mapping.

Database type:
MySQL
Foreign data type:
datetime
PostgreSQL data type:
timestamptz
Transformation direction:
Import
Transformation:
CASE
WHEN DATA = '0000-00-00 00:00:00'
THEN NULL
ELSE DATA
END

Here, I'm making the simplifying assumption that there is a bijective
mapping between data types.

Is there some way to fit the per-column part of such a mapping into
this scheme? We'd need to do some dependency tracking in order to be
able to point to the appropriate code...

IIUC, you are talking about using FDW options as storage of data
type mapping setting, or mapping definition itself, right? If so, a
foreign table needs to be created to use per-column FDW options.
Does it suit to your idea?

Yes. The only mildly disturbing thing about how that would work is
that "magic" key names would actually point to executable code, so
there would be some kind of non-uniform processing of the options, and
(possibly quite unlikely) ways to escalate privilege.

BTW, I couldn't get what you mean by "dependency tracking". You
mean the dependency between foreign column and local column? It
might include essence of your idea... Would you explain the detail?

I think the dependency between the mapping between the foreign column
and the local one is already handled. On that subject, it's possible
to make an argument that this mapping might need to be expanded so
that in general, M foreign columns map to N local ones (distinct M and
N), but that's a research topic, so let's not worry about it now.

The dependency tracking I have in mind is of the actual executable
code. If the inbound mapping has what amounts to a pointer to a
function, it shouldn't be possible to drop that function without
CASCADE, and if we're caching such functions, the cache needs to be
refreshed any time the function changes.

Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#7Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Shigeru Hanada (#3)
Re: per-column generic option

I checked your patch.

The backend portion seems to me OK, but I have several questions/comments.

* This patch should be rebased.
It conflicts with the latest bin/psql/describe.c and
include/catalog/catversion.h.
IIRC, we should not touch catversion.h in submission stage. (It might
be a task of
committer when a patch get upstreamed.)

* It might be an option to extend attreloptions, instead of the new
attfdwoptions.
Although I didn't track the discussion when pg_foreign_table catalog
that provides
relation level fdw-options, was it impossible or unreasonable to extend existing
design of reloptions/attoptions?
Right now, it accepts only hard-wired options listed at reloptions.c.
But, it seems
to me worthwhile, if it could accept options validated by loadable modules.

* pg_dump shall die when we run it for older postgresql version.

This patch does not modify queries to older postgresql version at
getTableAttrs().
In the result, this index shall be set by -1.
+ i_attfdwoptions = PQfnumber(res, "attfdwoptions");

Then, PGgetvalue() returns NULL for unranged column number, and strdup()
shall cause segmentation fault.
+ tbinfo->attfdwoptions[j] = strdup(PQgetvalue(res, j,
i_attfdwoptions));

In fact, I tried to run the patched pg_dump towards v9.0.2
[kaigai@vmlinux pg_dump]$ ./pg_dump postgres
pg_dump: column number -1 is out of range 0..14
Segmentation fault

My recommendation is to append "NULL as attfdwoptions" on the queries to
older versions. It eventually makes PGgetvalue() to return an empty string,
then strdup() does not cause a problem.

Thanks,

2011年6月15日10:57 Shigeru Hanada <shigeru.hanada@gmail.com>:

(2011/06/14 21:20), Robert Haas wrote:

I haven't looked at the patch yet, but here are a few comments on the
design, which overall looks good.

Thanks for the review. Please find attached a revised patch.

In addition to responding to your comments, I also added pg_dump
support. Now pg_dump dumps per-column generic options with ALTER
FOREIGN TABLE ALTER COLUMN statement just after CREATE FOREIGN TABLE.
Once I've though to dump them in each column definition of a CREATE
FOREIGN TABLE statement, but that seems to makes the statement too complex.

2011/6/14 Shigeru Hanada<shigeru.hanada@gmail.com>:

1) psql should support describing per-column generic options, so \dec
command was added. If the form \dec+ is used, generic options are also
displayed. Output sample is:

I would not add a new backslash command for this - it's unlikely to be
useful to see this information across all tables. It would be more
helpful to somehow (not sure of the details) incorporate this into the
output of running \d on a foreign table.

Hm, belatedly I noticed that relation-kind-specific column are added
preceding to verbose-only columns such as expression for indexes and
column values for sequences. It seems suitable place to show per-column
generic options. Please see attached "desc_results.txt" as sample.

I also noticed that relation-kind-specific information are not mentioned
in any document (at least in the section of psql[1]), even about
existing ones such as sequence values and index definition. I also
added short brief of them to psql document.

BTW, while working around \d command, I noticed that we can avoid
variable width (# of columns) query result, which is used to fetch
column information, with using NULL as placeholder (and it has already
been used partially). I think that it would enhance maintainability
little, so I've separated this fix to another patch
avoid_variable_width_result.patch. The main patch
per_column_option_v2.patch assumes that this fix has been applied.

[1] http://developer.postgresql.org/pgdocs/postgres/app-psql.html

Here I found an inconsistency about privilege to see generic options
(not only column but also FDW and server et al). The
information_schema.*_options only shows options which are associated to
objects that current user can access, but \de*+ doesn't have such
restriction. \de* commands should be fixed to hide forbidden objects?

It's less important whether \de* is consistent with information_schema
in this regard than it is whether it is consistent with other psql
backslash commands, e.g. \dv or \db or \dC. AFAIK those commands do
not filter by privilege.

Agreed, I'll leave \de* to show results unconditionally.

1) Is "generic options" proper term to mean FDW-specific option
associated to a FDW object? It's used in the SQL/MED standard, but
seems not popular... "FDW option" would be better than "generic option"?

I think FDW option is much clearer.

So do I, but I didn't touch them because "generic option" appears in
many documents, source files including comments and psql's \d* output.
Most of them have been there since 8.4. Is it acceptable to change them
to "FDW option", at least for only documents?

OTOH, psql's \d* commands use "Options" as column header of FDW options
and reloptions. I also left them because I thought that this would not
cause misunderstanding.

Regards,
--
Shigeru Hanada

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#8Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: Kohei KaiGai (#7)
1 attachment(s)
Re: per-column generic option

(2011/06/26 18:34), Kohei KaiGai wrote:

I checked your patch.

Thanks for the review! Please find attached a revised patch.

The backend portion seems to me OK, but I have several questions/comments.

* This patch should be rebased.
It conflicts with the latest bin/psql/describe.c and
include/catalog/catversion.h.
IIRC, we should not touch catversion.h in submission stage. (It might
be a task of
committer when a patch get upstreamed.)

I've rebased against current HEAD, and reverted catversion.h.

* It might be an option to extend attreloptions, instead of the new
attfdwoptions.
Although I didn't track the discussion when pg_foreign_table catalog
that provides
relation level fdw-options, was it impossible or unreasonable to extend existing
design of reloptions/attoptions?
Right now, it accepts only hard-wired options listed at reloptions.c.
But, it seems
to me worthwhile, if it could accept options validated by loadable modules.

IIRC someone has objected against storing FDW options in
reloptions/attoptions, but I couldn't find such post. I'll follow the
discussion again.

IMHO, though at present I don't have clear proof, separating FDW options
from access method options seems better than merging them, but I should
learn more about AM mechanism to clarify this issue. Please check other
issues first.

* pg_dump shall die when we run it for older postgresql version.

This patch does not modify queries to older postgresql version at
getTableAttrs().
In the result, this index shall be set by -1.
+ i_attfdwoptions = PQfnumber(res, "attfdwoptions");

Then, PGgetvalue() returns NULL for unranged column number, and strdup()
shall cause segmentation fault.
+ tbinfo->attfdwoptions[j] = strdup(PQgetvalue(res, j,
i_attfdwoptions));

In fact, I tried to run the patched pg_dump towards v9.0.2
[kaigai@vmlinux pg_dump]$ ./pg_dump postgres
pg_dump: column number -1 is out of range 0..14
Segmentation fault

My recommendation is to append "NULL as attfdwoptions" on the queries to
older versions. It eventually makes PGgetvalue() to return an empty string,
then strdup() does not cause a problem.

Fixed in the way you've recommended, and tested against 8.4. I should
have noticed that same technique is used in some other places...

BTW, I also have found an unnecessary FIXME comment and removed it.
Please see the line 2845 of src/backend/catalog/heap.c
(InsertPgAttributeTuple) for the correction.

Regards,
--
Shigeru Hanada

Attachments:

per_column_option_v3.patchtext/plain; name=per_column_option_v3.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 713ee25..ae5cf6b 100644
*** a/doc/src/sgml/catalogs.sgml
--- b/doc/src/sgml/catalogs.sgml
***************
*** 1152,1157 ****
--- 1152,1166 ----
        </entry>
       </row>
  
+      <row>
+       <entry><structfield>attfdwoptions</structfield></entry>
+       <entry><type>text[]</type></entry>
+       <entry></entry>
+       <entry>
+        Attribute-level generic options, as <quote>keyword=value</> strings
+       </entry>
+      </row>
+ 
      </tbody>
     </tgroup>
    </table>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index ab9ce2a..9bc0d3c 100644
*** a/doc/src/sgml/information_schema.sgml
--- b/doc/src/sgml/information_schema.sgml
***************
*** 958,963 ****
--- 958,1026 ----
    </table>
   </sect1>
  
+  <sect1 id="infoschema-column-options">
+   <title><literal>column_options</literal></title>
+ 
+   <para>
+    The view <literal>column_options</literal> contains all the
+    options defined for foreign table columns in the current database.  Only
+    those foreign table columns are shown that the current user has access to
+    (by way of being the owner or having some privilege).
+   </para>
+ 
+   <table>
+    <title><literal>column_options</literal> Columns</title>
+ 
+    <tgroup cols="3">
+     <thead>
+      <row>
+       <entry>Name</entry>
+       <entry>Data Type</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+ 
+     <tbody>
+      <row>
+       <entry><literal>table_catalog</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the database that contains the foreign table (always the current database)</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>table_schema</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the schema that contains the foreign table</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>table_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the foreign table</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>column_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the column</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>option_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of an option</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>option_value</literal></entry>
+       <entry><type>character_data</type></entry>
+       <entry>Value of the option</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect1>
+ 
   <sect1 id="infoschema-column-privileges">
    <title><literal>column_privileges</literal></title>
  
diff --git a/doc/src/sgml/ref/alter_foreign_table.sgml b/doc/src/sgml/ref/alter_foreign_table.sgml
index a45df02..95ae02a 100644
*** a/doc/src/sgml/ref/alter_foreign_table.sgml
--- b/doc/src/sgml/ref/alter_foreign_table.sgml
*************** ALTER FOREIGN TABLE <replaceable class="
*** 36,41 ****
--- 36,42 ----
      DROP [ COLUMN ] [ IF EXISTS ] <replaceable class="PARAMETER">column</replaceable> [ RESTRICT | CASCADE ]
      ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> [ SET DATA ] TYPE <replaceable class="PARAMETER">type</replaceable>
      ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> { SET | DROP } NOT NULL
+     ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
      OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
      OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
  </synopsis>
*************** ALTER FOREIGN TABLE <replaceable class="
*** 125,136 ****
      <term><literal>OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ] )</literal></term>
      <listitem>
       <para>
!       Change options for the foreign table.
        <literal>ADD</>, <literal>SET</>, and <literal>DROP</>
        specify the action to be performed.  <literal>ADD</> is assumed
!       if no operation is explicitly specified.  Option names must be
!       unique; names and values are also validated using the foreign
!       data wrapper library.
       </para>
      </listitem>
     </varlistentry>
--- 126,137 ----
      <term><literal>OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ] )</literal></term>
      <listitem>
       <para>
!       Change options for the foreign table or the column of the foreign table.
        <literal>ADD</>, <literal>SET</>, and <literal>DROP</>
        specify the action to be performed.  <literal>ADD</> is assumed
!       if no operation is explicitly specified.  Option names must be unique
!       in each associated object; names and values are also validated using the
!       foreign data wrapper library.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml
index ad91072..8863386 100644
*** a/doc/src/sgml/ref/create_foreign_table.sgml
--- b/doc/src/sgml/ref/create_foreign_table.sgml
***************
*** 19,25 ****
   <refsynopsisdiv>
  <synopsis>
  CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="PARAMETER">table_name</replaceable> ( [
!   { <replaceable class="PARAMETER">column_name</replaceable> <replaceable class="PARAMETER">data_type</replaceable> [ NULL | NOT NULL ] }
      [, ... ]
  ] )
    SERVER <replaceable class="parameter">server_name</replaceable>
--- 19,25 ----
   <refsynopsisdiv>
  <synopsis>
  CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="PARAMETER">table_name</replaceable> ( [
!   { <replaceable class="PARAMETER">column_name</replaceable> <replaceable class="PARAMETER">data_type</replaceable> [ OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ... ] ) ] [ NULL | NOT NULL ] }
      [, ... ]
  ] )
    SERVER <replaceable class="parameter">server_name</replaceable>
*************** CREATE FOREIGN TABLE [ IF NOT EXISTS ] <
*** 138,147 ****
      <term><literal>OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ...] )</literal></term>
      <listitem>
       <para>
!       Options to be associated with the new foreign table.
        The allowed option names and values are specific to each foreign
        data wrapper and are validated using the foreign-data wrapper's
!       validator function. Option names must be unique.
       </para>
      </listitem>
     </varlistentry>
--- 138,149 ----
      <term><literal>OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ...] )</literal></term>
      <listitem>
       <para>
!       Options to be associated with the new foreign table or the column of
!       the foreign table.
        The allowed option names and values are specific to each foreign
        data wrapper and are validated using the foreign-data wrapper's
!       validator function. Option names must be unique in each associated
!       object.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 132a7b3..74b26b2 100644
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** testdb=&gt;
*** 891,896 ****
--- 891,903 ----
          below.)
          </para>
  
+ 		<para>
+         In addition to common information, <literal>\d</> shows
+         relation-kind-specific information for each column:
+         column values for sequences, indexed expression for indexes and
+         per-column generic options for foreign tables.
+ 		</para>
+ 
          <para>
          The command form <literal>\d+</literal> is identical, except that
          more information is displayed: any comments associated with the
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 16979c4..4ffedab 100644
*** a/src/backend/access/common/tupdesc.c
--- b/src/backend/access/common/tupdesc.c
*************** equalTupleDescs(TupleDesc tupdesc1, Tupl
*** 362,368 ****
  			return false;
  		if (attr1->attcollation != attr2->attcollation)
  			return false;
! 		/* attacl and attoptions are not even present... */
  	}
  
  	if (tupdesc1->constr != NULL)
--- 362,368 ----
  			return false;
  		if (attr1->attcollation != attr2->attcollation)
  			return false;
! 		/* attacl, attoptions and attfdwoptions are not even present... */
  	}
  
  	if (tupdesc1->constr != NULL)
*************** TupleDescInitEntry(TupleDesc desc,
*** 482,488 ****
  	att->attisdropped = false;
  	att->attislocal = true;
  	att->attinhcount = 0;
! 	/* attacl and attoptions are not present in tupledescs */
  
  	tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(oidtypeid));
  	if (!HeapTupleIsValid(tuple))
--- 482,488 ----
  	att->attisdropped = false;
  	att->attislocal = true;
  	att->attinhcount = 0;
! 	/* attacl, attoptions and attfdwoptions are not present in tupledescs */
  
  	tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(oidtypeid));
  	if (!HeapTupleIsValid(tuple))
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index 0aeaf5b..d91af52 100644
*** a/src/backend/catalog/genbki.pl
--- b/src/backend/catalog/genbki.pl
*************** sub emit_pgattr_row
*** 369,375 ****
          attislocal    => 't',
          attinhcount   => '0',
          attacl        => '_null_',
!         attoptions    => '_null_'
      );
      return {%PGATTR_DEFAULTS, %row};
  }
--- 369,376 ----
          attislocal    => 't',
          attinhcount   => '0',
          attacl        => '_null_',
!         attoptions    => '_null_',
!         attfdwoptions => '_null_'
      );
      return {%PGATTR_DEFAULTS, %row};
  }
*************** sub emit_schemapg_row
*** 400,405 ****
--- 401,407 ----
      # Only the fixed-size portions of the descriptors are ever used.
      delete $row->{attacl};
      delete $row->{attoptions};
+     delete $row->{attfdwoptions};
  
      # Expand booleans from 'f'/'t' to 'false'/'true'.
      # Some values might be other macros (eg FLOAT4PASSBYVAL), don't change.
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index e606ac2..dd0f4fd 100644
*** a/src/backend/catalog/heap.c
--- b/src/backend/catalog/heap.c
*************** static List *insert_ordered_unique_oid(L
*** 126,132 ****
   */
  
  /*
!  * The initializers below do not include the attoptions or attacl fields,
   * but that's OK - we're never going to reference anything beyond the
   * fixed-size portion of the structure anyway.
   */
--- 126,132 ----
   */
  
  /*
!  * The initializers below do not include trailing variable length fields,
   * but that's OK - we're never going to reference anything beyond the
   * fixed-size portion of the structure anyway.
   */
*************** InsertPgAttributeTuple(Relation pg_attri
*** 607,612 ****
--- 607,613 ----
  	/* start out with empty permissions and empty options */
  	nulls[Anum_pg_attribute_attacl - 1] = true;
  	nulls[Anum_pg_attribute_attoptions - 1] = true;
+ 	nulls[Anum_pg_attribute_attfdwoptions - 1] = true;
  
  	tup = heap_form_tuple(RelationGetDescr(pg_attribute_rel), values, nulls);
  
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 9334c76..4292d88 100644
*** a/src/backend/catalog/information_schema.sql
--- b/src/backend/catalog/information_schema.sql
*************** GRANT SELECT ON element_types TO PUBLIC;
*** 2516,2521 ****
--- 2516,2554 ----
  
  -- SQL/MED views; these use section numbers from part 9 of the standard.
  
+ /* Base view for foreign table columns */
+ CREATE VIEW _pg_foreign_table_columns AS
+     SELECT n.nspname,
+            c.relname,
+            a.attname,
+            a.attfdwoptions
+     FROM pg_foreign_table t, pg_authid u, pg_namespace n, pg_class c,
+          pg_attribute a
+     WHERE u.oid = c.relowner
+           AND (pg_has_role(c.relowner, 'USAGE')
+                OR has_column_privilege(c.oid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'))
+           AND n.oid = c.relnamespace
+           AND c.oid = t.ftrelid
+           AND c.relkind = 'f'
+           AND a.attrelid = c.oid
+           AND a.attnum > 0;
+ 
+ /*
+  * 24.2
+  * COLUMN_OPTIONS view
+  */
+ CREATE VIEW column_options AS
+     SELECT CAST(current_database() AS sql_identifier) AS table_catalog,
+            c.nspname AS table_schema,
+            c.relname AS table_name,
+            c.attname AS column_name,
+            CAST((pg_options_to_table(c.attfdwoptions)).option_name AS sql_identifier) AS option_name,
+            CAST((pg_options_to_table(c.attfdwoptions)).option_value AS character_data) AS option_value
+     FROM _pg_foreign_table_columns c;
+ 
+ GRANT SELECT ON column_options TO PUBLIC;
+ 
+ 
  /* Base view for foreign-data wrappers */
  CREATE VIEW _pg_foreign_data_wrappers AS
      SELECT w.oid,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b2ba11c..88c54d4 100644
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** static void ATPrepAlterColumnType(List *
*** 344,349 ****
--- 344,350 ----
  static bool ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno);
  static void ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
  					  AlterTableCmd *cmd, LOCKMODE lockmode);
+ static void ATExecAlterColumnGenericOptions(Relation rel, const char *colName, List *options, LOCKMODE lockmode);
  static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode);
  static void ATPostAlterTypeParse(char *cmd, List **wqueue, LOCKMODE lockmode);
  static void change_owner_recurse_to_sequences(Oid relationOid,
*************** AlterTableGetLockLevel(List *cmds)
*** 2607,2612 ****
--- 2608,2614 ----
  			case AT_DropNotNull:		/* may change some SQL plans */
  			case AT_SetNotNull:
  			case AT_GenericOptions:
+ 			case AT_AlterColumnGenericOptions:
  				cmd_lockmode = AccessExclusiveLock;
  				break;
  
*************** ATPrepCmd(List **wqueue, Relation rel, A
*** 2881,2886 ****
--- 2883,2894 ----
  			ATPrepAlterColumnType(wqueue, tab, rel, recurse, recursing, cmd, lockmode);
  			pass = AT_PASS_ALTER_TYPE;
  			break;
+ 		case AT_AlterColumnGenericOptions:
+ 			ATSimplePermissions(rel, ATT_FOREIGN_TABLE);
+ 			/* This command never recurses */
+ 			/* No command-specific prep needed */
+ 			pass = AT_PASS_MISC;
+ 			break;
  		case AT_ChangeOwner:	/* ALTER OWNER */
  			/* This command never recurses */
  			/* No command-specific prep needed */
*************** ATExecCmd(List **wqueue, AlteredTableInf
*** 3114,3119 ****
--- 3122,3130 ----
  		case AT_AlterColumnType:		/* ALTER COLUMN TYPE */
  			ATExecAlterColumnType(tab, rel, cmd, lockmode);
  			break;
+ 		case AT_AlterColumnGenericOptions:	/* ALTER COLUMN OPTIONS */
+ 			ATExecAlterColumnGenericOptions(rel, cmd->name, (List *) cmd->def, lockmode);
+ 			break;
  		case AT_ChangeOwner:	/* ALTER OWNER */
  			ATExecChangeOwner(RelationGetRelid(rel),
  							  get_role_oid(cmd->name, false),
*************** ATExecAlterColumnType(AlteredTableInfo *
*** 7168,7173 ****
--- 7179,7278 ----
  	heap_freetuple(heapTup);
  }
  
+ static void
+ ATExecAlterColumnGenericOptions(Relation rel,
+ 								const char *colName,
+ 								List *options,
+ 								LOCKMODE lockmode)
+ {
+ 	Relation	ftrel;
+ 	Relation	attrel;
+ 	ForeignServer *server;
+ 	ForeignDataWrapper *fdw;
+ 	HeapTuple	tuple;
+ 	HeapTuple	newtuple;
+ 	bool		isnull;
+ 	Datum		repl_val[Natts_pg_attribute];
+ 	bool		repl_null[Natts_pg_attribute];
+ 	bool		repl_repl[Natts_pg_attribute];
+ 	Datum		datum;
+ 	Form_pg_foreign_table fttableform;
+ 	Form_pg_attribute atttableform;
+ 
+ 	if (options == NIL)
+ 		return;
+ 
+ 	/* First, determine FDW validator associated to the foreign table. */
+ 	ftrel = heap_open(ForeignTableRelationId, AccessShareLock);
+ 	tuple = SearchSysCache1(FOREIGNTABLEREL, rel->rd_id);
+ 	if (!HeapTupleIsValid(tuple))
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_UNDEFINED_OBJECT),
+ 				 errmsg("foreign table \"%s\" does not exist",
+ 						RelationGetRelationName(rel))));
+ 	fttableform = (Form_pg_foreign_table) GETSTRUCT(tuple);
+ 	server = GetForeignServer(fttableform->ftserver);
+ 	fdw = GetForeignDataWrapper(server->fdwid);
+ 
+ 	heap_close(ftrel, AccessShareLock);
+ 	ReleaseSysCache(tuple);
+ 
+ 	attrel = heap_open(AttributeRelationId, RowExclusiveLock);
+ 	tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName);
+ 	if (!HeapTupleIsValid(tuple))
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_UNDEFINED_COLUMN),
+ 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+ 						colName, RelationGetRelationName(rel))));
+ 
+ 	/* Prevent them from altering a system attribute */
+ 	atttableform = (Form_pg_attribute) GETSTRUCT(tuple);
+ 	if (atttableform->attnum <= 0)
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ 				 errmsg("cannot alter system column \"%s\"", colName)));
+ 
+ 
+ 	/* Initialize buffers for new tuple values */
+ 	memset(repl_val, 0, sizeof(repl_val));
+ 	memset(repl_null, false, sizeof(repl_null));
+ 	memset(repl_repl, false, sizeof(repl_repl));
+ 
+ 	/* Extract the current options */
+ 	datum = SysCacheGetAttr(ATTNAME,
+ 							tuple,
+ 							Anum_pg_attribute_attfdwoptions,
+ 							&isnull);
+ 	if (isnull)
+ 		datum = PointerGetDatum(NULL);
+ 
+ 	/* Transform the options */
+ 	datum = transformGenericOptions(AttributeRelationId,
+ 									datum,
+ 									options,
+ 									fdw->fdwvalidator);
+ 
+ 	if (PointerIsValid(DatumGetPointer(datum)))
+ 		repl_val[Anum_pg_attribute_attfdwoptions - 1] = datum;
+ 	else
+ 		repl_null[Anum_pg_attribute_attfdwoptions - 1] = true;
+ 
+ 	repl_repl[Anum_pg_attribute_attfdwoptions - 1] = true;
+ 
+ 	/* Everything looks good - update the tuple */
+ 
+ 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrel),
+ 								 repl_val, repl_null, repl_repl);
+ 	ReleaseSysCache(tuple);
+ 
+ 	simple_heap_update(attrel, &newtuple->t_self, newtuple);
+ 	CatalogUpdateIndexes(attrel, newtuple);
+ 
+ 	heap_close(attrel, RowExclusiveLock);
+ 
+ 	heap_freetuple(newtuple);
+ }
+ 
  /*
   * Cleanup after we've finished all the ALTER TYPE operations for a
   * particular relation.  We have to drop and recreate all the indexes
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c9133dd..148110f 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyColumnDef(ColumnDef *from)
*** 2312,2317 ****
--- 2312,2318 ----
  	COPY_NODE_FIELD(collClause);
  	COPY_SCALAR_FIELD(collOid);
  	COPY_NODE_FIELD(constraints);
+ 	COPY_NODE_FIELD(fdwoptions);
  
  	return newnode;
  }
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 681f5f8..cb9be13 100644
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outColumnDef(StringInfo str, ColumnDef 
*** 2101,2106 ****
--- 2101,2107 ----
  	WRITE_NODE_FIELD(collClause);
  	WRITE_OID_FIELD(collOid);
  	WRITE_NODE_FIELD(constraints);
+ 	WRITE_NODE_FIELD(fdwoptions);
  }
  
  static void
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 62cff8a..da56959 100644
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** alter_table_cmd:
*** 1769,1774 ****
--- 1769,1783 ----
  					def->raw_default = $8;
  					$$ = (Node *)n;
  				}
+ 			/* ALTER FOREIGN TABLE <name> ALTER [COLUMN] <colname> OPTIONS */
+ 			| ALTER opt_column ColId alter_generic_options
+ 				{
+ 					AlterTableCmd *n = makeNode(AlterTableCmd);
+ 					n->subtype = AT_AlterColumnGenericOptions;
+ 					n->name = $3;
+ 					n->def = (Node *) $4;
+ 					$$ = (Node *)n;
+ 				}
  			/* ALTER TABLE <name> ADD CONSTRAINT ... */
  			| ADD_P TableConstraint
  				{
*************** TypedTableElement:
*** 2497,2503 ****
  			| TableConstraint					{ $$ = $1; }
  		;
  
! columnDef:	ColId Typename ColQualList
  				{
  					ColumnDef *n = makeNode(ColumnDef);
  					n->colname = $1;
--- 2506,2512 ----
  			| TableConstraint					{ $$ = $1; }
  		;
  
! columnDef:	ColId Typename create_generic_options ColQualList
  				{
  					ColumnDef *n = makeNode(ColumnDef);
  					n->colname = $1;
*************** columnDef:	ColId Typename ColQualList
*** 2510,2516 ****
  					n->raw_default = NULL;
  					n->cooked_default = NULL;
  					n->collOid = InvalidOid;
! 					SplitColQualList($3, &n->constraints, &n->collClause,
  									 yyscanner);
  					$$ = (Node *)n;
  				}
--- 2519,2526 ----
  					n->raw_default = NULL;
  					n->cooked_default = NULL;
  					n->collOid = InvalidOid;
! 					n->fdwoptions = $3;
! 					SplitColQualList($4, &n->constraints, &n->collClause,
  									 yyscanner);
  					$$ = (Node *)n;
  				}
*************** AlterFdwStmt: ALTER FOREIGN DATA_P WRAPP
*** 3680,3686 ****
  /* Options definition for CREATE FDW, SERVER and USER MAPPING */
  create_generic_options:
  			OPTIONS '(' generic_option_list ')'			{ $$ = $3; }
! 			| /*EMPTY*/									{ $$ = NIL; }
  		;
  
  generic_option_list:
--- 3690,3696 ----
  /* Options definition for CREATE FDW, SERVER and USER MAPPING */
  create_generic_options:
  			OPTIONS '(' generic_option_list ')'			{ $$ = $3; }
! 			| /*EMPTY*/									{ $$ = NIL }
  		;
  
  generic_option_list:
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 8744654..8f1f615 100644
*** a/src/backend/parser/parse_utilcmd.c
--- b/src/backend/parser/parse_utilcmd.c
*************** transformColumnDefinition(CreateStmtCont
*** 555,560 ****
--- 555,591 ----
  				break;
  		}
  	}
+ 
+ 	/*
+ 	 * Generate ALTER FOREIGN TABLE ALTER COLUMN statement which adds 
+ 	 * per-column generic options for this column.
+ 	 */
+ 	if (column->fdwoptions != NIL)
+ 	{
+ 		AlterTableStmt *stmt;
+ 		AlterTableCmd  *cmd;
+ 
+ 		cmd = makeNode(AlterTableCmd);
+ 		cmd->subtype = AT_AlterColumnGenericOptions;
+ 		cmd->name = column->colname;
+ 		cmd->def = (Node *) column->fdwoptions;
+ 		cmd->behavior = DROP_RESTRICT;
+ 		cmd->missing_ok = false;
+ 
+ 		stmt = makeNode(AlterTableStmt);
+ 		stmt->relation = cxt->relation;
+ 		stmt->cmds = NIL;
+ 		stmt->relkind = OBJECT_FOREIGN_TABLE;
+ 		stmt->cmds = lappend(stmt->cmds, cmd);
+ 
+ 		cxt->alist = lappend(cxt->alist, stmt);
+ 
+ 		foreach (clist, column->fdwoptions)
+ 		{
+ 			DefElem		   *option = (DefElem *) lfirst(clist);
+ 			elog(DEBUG3, "%s=%s", option->defname, strVal(option->arg));
+ 		}
+ 	}
  }
  
  /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 9e69b0f..258e153 100644
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5556,5561 ****
--- 5556,5562 ----
  	int			i_attislocal;
  	int			i_attoptions;
  	int			i_attcollation;
+ 	int			i_attfdwoptions;
  	PGresult   *res;
  	int			ntups;
  	bool		hasdefaults;
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5593,5599 ****
  
  		resetPQExpBuffer(q);
  
! 		if (g_fout->remoteVersion >= 90100)
  		{
  			/*
  			 * attcollation is new in 9.1.	Since we only want to dump COLLATE
--- 5594,5624 ----
  
  		resetPQExpBuffer(q);
  
! 		if (g_fout->remoteVersion >= 90200)
! 		{
! 			/*
! 			 * attfdwoptions is new in 9.2.
! 			 */
! 			appendPQExpBuffer(q, "SELECT a.attnum, a.attname, a.atttypmod, "
! 							  "a.attstattarget, a.attstorage, t.typstorage, "
! 							  "a.attnotnull, a.atthasdef, a.attisdropped, "
! 							  "a.attlen, a.attalign, a.attislocal, "
! 				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
! 						"array_to_string(a.attoptions, ', ') AS attoptions, "
! 							  "CASE WHEN a.attcollation <> t.typcollation "
! 							"THEN a.attcollation ELSE 0 END AS attcollation, "
! 				  "array_to_string(ARRAY("
! 				  "  SELECT option_name || ' ' || quote_literal(option_value) "
! 				  "  FROM pg_options_to_table(attfdwoptions)), ', ') "
! 				  " AS attfdwoptions "
! 			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
! 							  "ON a.atttypid = t.oid "
! 							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
! 							  "AND a.attnum > 0::pg_catalog.int2 "
! 							  "ORDER BY a.attrelid, a.attnum",
! 							  tbinfo->dobj.catId.oid);
! 		}
! 		else if (g_fout->remoteVersion >= 90100)
  		{
  			/*
  			 * attcollation is new in 9.1.	Since we only want to dump COLLATE
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5608,5614 ****
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
  						"array_to_string(a.attoptions, ', ') AS attoptions, "
  							  "CASE WHEN a.attcollation <> t.typcollation "
! 							"THEN a.attcollation ELSE 0 END AS attcollation "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
--- 5633,5640 ----
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
  						"array_to_string(a.attoptions, ', ') AS attoptions, "
  							  "CASE WHEN a.attcollation <> t.typcollation "
! 							"THEN a.attcollation ELSE 0 END AS attcollation, "
! 							  "NULL AS attfdwoptions "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5616,5621 ****
--- 5642,5648 ----
  							  "ORDER BY a.attrelid, a.attnum",
  							  tbinfo->dobj.catId.oid);
  		}
+ 
  		else if (g_fout->remoteVersion >= 90000)
  		{
  			/* attoptions is new in 9.0 */
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5625,5631 ****
  							  "a.attlen, a.attalign, a.attislocal, "
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
  						"array_to_string(a.attoptions, ', ') AS attoptions, "
! 							  "0 AS attcollation "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
--- 5652,5659 ----
  							  "a.attlen, a.attalign, a.attislocal, "
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
  						"array_to_string(a.attoptions, ', ') AS attoptions, "
! 							  "0 AS attcollation, "
! 							  "NULL AS attfdwoptions "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5641,5647 ****
  							  "a.attnotnull, a.atthasdef, a.attisdropped, "
  							  "a.attlen, a.attalign, a.attislocal, "
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
--- 5669,5676 ----
  							  "a.attnotnull, a.atthasdef, a.attisdropped, "
  							  "a.attlen, a.attalign, a.attislocal, "
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation, "
! 							  "NULL AS attfdwoptions "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5662,5668 ****
  							  "false AS attisdropped, a.attlen, "
  							  "a.attalign, false AS attislocal, "
  							  "format_type(t.oid,a.atttypmod) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation "
  							  "FROM pg_attribute a LEFT JOIN pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::oid "
--- 5691,5698 ----
  							  "false AS attisdropped, a.attlen, "
  							  "a.attalign, false AS attislocal, "
  							  "format_type(t.oid,a.atttypmod) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation, "
! 							  "NULL AS attfdwoptions "
  							  "FROM pg_attribute a LEFT JOIN pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::oid "
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5680,5686 ****
  							  "attlen, attalign, "
  							  "false AS attislocal, "
  							  "(SELECT typname FROM pg_type WHERE oid = atttypid) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation "
  							  "FROM pg_attribute a "
  							  "WHERE attrelid = '%u'::oid "
  							  "AND attnum > 0::int2 "
--- 5710,5717 ----
  							  "attlen, attalign, "
  							  "false AS attislocal, "
  							  "(SELECT typname FROM pg_type WHERE oid = atttypid) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation, "
! 							  "NULL AS attfdwoptions "
  							  "FROM pg_attribute a "
  							  "WHERE attrelid = '%u'::oid "
  							  "AND attnum > 0::int2 "
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5708,5713 ****
--- 5739,5745 ----
  		i_attislocal = PQfnumber(res, "attislocal");
  		i_attoptions = PQfnumber(res, "attoptions");
  		i_attcollation = PQfnumber(res, "attcollation");
+ 		i_attfdwoptions = PQfnumber(res, "attfdwoptions");
  
  		tbinfo->numatts = ntups;
  		tbinfo->attnames = (char **) malloc(ntups * sizeof(char *));
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5724,5729 ****
--- 5756,5762 ----
  		tbinfo->attrdefs = (AttrDefInfo **) malloc(ntups * sizeof(AttrDefInfo *));
  		tbinfo->attoptions = (char **) malloc(ntups * sizeof(char *));
  		tbinfo->attcollation = (Oid *) malloc(ntups * sizeof(Oid));
+ 		tbinfo->attfdwoptions = (char **) malloc(ntups * sizeof(char *));
  		tbinfo->inhAttrs = (bool *) malloc(ntups * sizeof(bool));
  		tbinfo->inhAttrDef = (bool *) malloc(ntups * sizeof(bool));
  		tbinfo->inhNotNull = (bool *) malloc(ntups * sizeof(bool));
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5750,5755 ****
--- 5783,5789 ----
  			tbinfo->notnull[j] = (PQgetvalue(res, j, i_attnotnull)[0] == 't');
  			tbinfo->attoptions[j] = strdup(PQgetvalue(res, j, i_attoptions));
  			tbinfo->attcollation[j] = atooid(PQgetvalue(res, j, i_attcollation));
+ 			tbinfo->attfdwoptions[j] = strdup(PQgetvalue(res, j, i_attfdwoptions));
  			tbinfo->attrdefs[j] = NULL; /* fix below */
  			if (PQgetvalue(res, j, i_atthasdef)[0] == 't')
  				hasdefaults = true;
*************** dumpTableSchema(Archive *fout, TableInfo
*** 12451,12456 ****
--- 12485,12505 ----
  				appendPQExpBuffer(q, "SET (%s);\n",
  								  tbinfo->attoptions[j]);
  			}
+ 
+ 			/*
+ 			 * Dump per-column generic options.
+ 			 */
+ 			if (tbinfo->relkind == RELKIND_FOREIGN_TABLE &&
+ 				tbinfo->attfdwoptions[j] &&
+ 				tbinfo->attfdwoptions[j][0] != '\0')
+ 			{
+ 				appendPQExpBuffer(q, "ALTER FOREIGN TABLE %s ",
+ 								  fmtId(tbinfo->dobj.name));
+ 				appendPQExpBuffer(q, "ALTER COLUMN %s ",
+ 								  fmtId(tbinfo->attnames[j]));
+ 				appendPQExpBuffer(q, "OPTIONS (%s);\n",
+ 								  tbinfo->attfdwoptions[j]);
+ 			}
  		}
  	}
  
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index c95614b..0a65401 100644
*** a/src/bin/pg_dump/pg_dump.h
--- b/src/bin/pg_dump/pg_dump.h
*************** typedef struct _tableInfo
*** 275,280 ****
--- 275,281 ----
  	bool	   *attislocal;		/* true if attr has local definition */
  	char	  **attoptions;		/* per-attribute options */
  	Oid		   *attcollation;	/* per-attribute collation selection */
+ 	char	  **attfdwoptions;	/* per-attribute generic options */
  
  	/*
  	 * Note: we need to store per-attribute notnull, default, and constraint
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index b2c54b5..381150c 100644
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** describeOneTableDetails(const char *sche
*** 1281,1287 ****
  		res = NULL;
  	}
  
! 	/* Get column info */
  	printfPQExpBuffer(&buf, "SELECT a.attname,");
  	appendPQExpBuffer(&buf, "\n  pg_catalog.format_type(a.atttypid, a.atttypmod),"
  					  "\n  (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)"
--- 1281,1292 ----
  		res = NULL;
  	}
  
! 	/*
! 	 * Get column info
! 	 *
! 	 * You need to modify value of "firstvcol" which willbe defined below if
! 	 * you are adding column(s) preceding to verbose-only columns.
! 	 */
  	printfPQExpBuffer(&buf, "SELECT a.attname,");
  	appendPQExpBuffer(&buf, "\n  pg_catalog.format_type(a.atttypid, a.atttypmod),"
  					  "\n  (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)"
*************** describeOneTableDetails(const char *sche
*** 1295,1300 ****
--- 1300,1311 ----
  		appendPQExpBuffer(&buf, "\n  NULL AS attcollation");
  	if (tableinfo.relkind == 'i')
  		appendPQExpBuffer(&buf, ",\n  pg_catalog.pg_get_indexdef(a.attrelid, a.attnum, TRUE) AS indexdef");
+ 	else
+ 		appendPQExpBuffer(&buf, ",\n  NULL AS indexdef");
+ 	if (tableinfo.relkind == 'f' && pset.sversion >= 90200)
+ 		appendPQExpBuffer(&buf, ",\n  a.attfdwoptions");
+ 	else
+ 		appendPQExpBuffer(&buf, ",\n  NULL AS attfdwoptions");
  	if (verbose)
  		appendPQExpBuffer(&buf, ",\n  a.attstorage, pg_catalog.col_description(a.attrelid, a.attnum)");
  	appendPQExpBuffer(&buf, "\nFROM pg_catalog.pg_attribute a");
*************** describeOneTableDetails(const char *sche
*** 1376,1381 ****
--- 1387,1395 ----
  	if (tableinfo.relkind == 'i')
  		headers[cols++] = gettext_noop("Definition");
  
+ 	if (tableinfo.relkind == 'f' && pset.sversion >= 90200)
+ 		headers[cols++] = gettext_noop("Options");
+ 
  	if (verbose)
  	{
  		headers[cols++] = gettext_noop("Storage");
*************** describeOneTableDetails(const char *sche
*** 1458,1467 ****
  		if (tableinfo.relkind == 'i')
  			printTableAddCell(&cont, PQgetvalue(res, i, 6), false, false);
  
  		/* Storage and Description */
  		if (verbose)
  		{
! 			int			firstvcol = (tableinfo.relkind == 'i' ? 7 : 6);
  			char	   *storage = PQgetvalue(res, i, firstvcol);
  
  			/* these strings are literal in our syntax, so not translated. */
--- 1472,1485 ----
  		if (tableinfo.relkind == 'i')
  			printTableAddCell(&cont, PQgetvalue(res, i, 6), false, false);
  
+ 		/* FDW options for foreign table column, only for 9.2 or later */
+ 		if (tableinfo.relkind == 'f' && pset.sversion >= 90200)
+ 			printTableAddCell(&cont, PQgetvalue(res, i, 7), false, false);
+ 
  		/* Storage and Description */
  		if (verbose)
  		{
! 			int			firstvcol = 8;
  			char	   *storage = PQgetvalue(res, i, firstvcol);
  
  			/* these strings are literal in our syntax, so not translated. */
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index fb86d1e..b6bc472 100644
*** a/src/bin/psql/describe.h
--- b/src/bin/psql/describe.h
*************** extern bool listUserMappings(const char 
*** 87,92 ****
--- 87,95 ----
  /* \det */
  extern bool listForeignTables(const char *pattern, bool verbose);
  
+ /* \dec */
+ extern bool listForeignTableColumns(const char *pattern, bool verbose);
+ 
  /* \dL */
  extern bool listLanguages(const char *pattern, bool verbose, bool showSystem);
  
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 409d6ea..3ea87e8 100644
*** a/src/include/catalog/pg_attribute.h
--- b/src/include/catalog/pg_attribute.h
*************** CATALOG(pg_attribute,1249) BKI_BOOTSTRAP
*** 156,161 ****
--- 156,164 ----
  
  	/* Column-level options */
  	text		attoptions[1];
+ 
+ 	/* Column-level FDW options */
+ 	text		attfdwoptions[1];
  } FormData_pg_attribute;
  
  /*
*************** typedef FormData_pg_attribute *Form_pg_a
*** 179,185 ****
   * ----------------
   */
  
! #define Natts_pg_attribute				20
  #define Anum_pg_attribute_attrelid		1
  #define Anum_pg_attribute_attname		2
  #define Anum_pg_attribute_atttypid		3
--- 182,188 ----
   * ----------------
   */
  
! #define Natts_pg_attribute				21
  #define Anum_pg_attribute_attrelid		1
  #define Anum_pg_attribute_attname		2
  #define Anum_pg_attribute_atttypid		3
*************** typedef FormData_pg_attribute *Form_pg_a
*** 200,205 ****
--- 203,209 ----
  #define Anum_pg_attribute_attcollation	18
  #define Anum_pg_attribute_attacl		19
  #define Anum_pg_attribute_attoptions	20
+ #define Anum_pg_attribute_attfdwoptions	21
  
  
  /* ----------------
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 002ae6b..e006180 100644
*** a/src/include/catalog/pg_class.h
--- b/src/include/catalog/pg_class.h
*************** typedef FormData_pg_class *Form_pg_class
*** 132,138 ****
  /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
  DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
! DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 20 0 f f f f f 3 _null_ _null_ ));
  DESCR("");
  DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 26 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
--- 132,138 ----
  /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
  DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
! DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
  DESCR("");
  DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 26 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index c65e3cd..19dff9b 100644
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct ColumnDef
*** 500,505 ****
--- 500,506 ----
  	CollateClause *collClause;	/* untransformed COLLATE spec, if any */
  	Oid			collOid;		/* collation OID (InvalidOid if not set) */
  	List	   *constraints;	/* other constraints on column */
+ 	List	   *fdwoptions;		/* per-column FDW options */
  } ColumnDef;
  
  /*
*************** typedef enum AlterTableType
*** 1196,1201 ****
--- 1197,1203 ----
  	AT_DropConstraint,			/* drop constraint */
  	AT_DropConstraintRecurse,	/* internal to commands/tablecmds.c */
  	AT_AlterColumnType,			/* alter column type */
+ 	AT_AlterColumnGenericOptions,	/* alter column OPTIONS (...) */
  	AT_ChangeOwner,				/* change owner */
  	AT_ClusterOn,				/* CLUSTER ON */
  	AT_DropCluster,				/* SET WITHOUT CLUSTER */
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index def850b..a298a9c 100644
*** a/src/test/regress/expected/foreign_data.out
--- b/src/test/regress/expected/foreign_data.out
*************** ERROR:  syntax error at or near "WITH OI
*** 646,664 ****
  LINE 1: CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;
                                                ^
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer NOT NULL,
! 	c2 text,
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
  COMMENT ON COLUMN ft1.c1 IS 'ft1.c1';
  \d+ ft1
!               Foreign table "public.ft1"
!  Column |  Type   | Modifiers | Storage  | Description 
! --------+---------+-----------+----------+-------------
!  c1     | integer | not null  | plain    | ft1.c1
!  c2     | text    |           | extended | 
!  c3     | date    |           | plain    | 
  Server: sc
  Has OIDs: no
  
--- 646,664 ----
  LINE 1: CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;
                                                ^
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer OPTIONS (param1 'val1') NOT NULL,
! 	c2 text OPTIONS (param2 'val2', param3 'val3'),
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
  COMMENT ON COLUMN ft1.c1 IS 'ft1.c1';
  \d+ ft1
!                             Foreign table "public.ft1"
!  Column |  Type   | Modifiers |          Options          | Storage  | Description 
! --------+---------+-----------+---------------------------+----------+-------------
!  c1     | integer | not null  | {param1=val1}             | plain    | ft1.c1
!  c2     | text    |           | {param2=val2,param3=val3} | extended | 
!  c3     | date    |           |                           | plain    | 
  Server: sc
  Has OIDs: no
  
*************** ALTER FOREIGN TABLE ft1 ADD COLUMN c6 in
*** 687,693 ****
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer;
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ERROR:  "ft1" is not a table or view
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
--- 687,693 ----
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1');
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ERROR:  "ft1" is not a table or view
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
*************** ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 
*** 698,703 ****
--- 698,724 ----
  ERROR:  ALTER TYPE USING is only supported on plain tables
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE text;
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN xmin OPTIONS (ADD p1 'v1'); -- ERROR
+ ERROR:  cannot alter system column "xmin"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
+                         ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ \d+ ft1
+                             Foreign table "public.ft1"
+  Column |  Type   | Modifiers |          Options          | Storage  | Description 
+ --------+---------+-----------+---------------------------+----------+-------------
+  c1     | integer | not null  | {param1=val1}             | plain    | 
+  c2     | text    |           | {param2=val2,param3=val3} | extended | 
+  c3     | date    |           |                           | plain    | 
+  c4     | integer |           |                           | plain    | 
+  c6     | integer | not null  |                           | plain    | 
+  c7     | integer |           | {p1=v1,p2=v2}             | plain    | 
+  c8     | text    |           | {p2=V2}                   | extended | 
+  c9     | integer |           |                           | plain    | 
+  c10    | integer |           | {p1=v1}                   | plain    | 
+ Server: sc
+ Has OIDs: no
+ 
  -- can't change the column type if it's used elsewhere
  CREATE TABLE use_ft1_column_type (x ft1);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE integer;	-- ERROR
*************** ERROR:  relation "ft1" does not exist
*** 726,742 ****
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME c1 TO foreign_column_1;
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME TO foreign_table_1;
  \d foreign_schema.foreign_table_1
! Foreign table "foreign_schema.foreign_table_1"
!       Column      |  Type   | Modifiers 
! ------------------+---------+-----------
!  foreign_column_1 | integer | not null
!  c2               | text    | 
!  c3               | date    | 
!  c4               | integer | 
!  c6               | integer | not null
!  c7               | integer | 
!  c8               | text    | 
!  c10              | integer | 
  Server: sc
  
  -- Information schema
--- 747,763 ----
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME c1 TO foreign_column_1;
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME TO foreign_table_1;
  \d foreign_schema.foreign_table_1
!            Foreign table "foreign_schema.foreign_table_1"
!       Column      |  Type   | Modifiers |          Options          
! ------------------+---------+-----------+---------------------------
!  foreign_column_1 | integer | not null  | {param1=val1}
!  c2               | text    |           | {param2=val2,param3=val3}
!  c3               | date    |           | 
!  c4               | integer |           | 
!  c6               | integer | not null  | 
!  c7               | integer |           | {p1=v1,p2=v2}
!  c8               | text    |           | {p2=V2}
!  c10              | integer |           | {p1=v1}
  Server: sc
  
  -- Information schema
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index d323921..5d9b24a 100644
*** a/src/test/regress/sql/foreign_data.sql
--- b/src/test/regress/sql/foreign_data.sql
*************** CREATE FOREIGN TABLE ft1 () SERVER no_se
*** 264,271 ****
  CREATE FOREIGN TABLE ft1 (c1 serial) SERVER sc;                 -- ERROR
  CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;                -- ERROR
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer NOT NULL,
! 	c2 text,
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
--- 264,271 ----
  CREATE FOREIGN TABLE ft1 (c1 serial) SERVER sc;                 -- ERROR
  CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;                -- ERROR
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer OPTIONS (param1 'val1') NOT NULL,
! 	c2 text OPTIONS (param2 'val2', param3 'val3'),
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
*************** ALTER FOREIGN TABLE ft1 ADD COLUMN c6 in
*** 288,294 ****
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer;
  
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
--- 288,294 ----
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1');
  
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
*************** ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 
*** 297,302 ****
--- 297,307 ----
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10) using '0'; -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE text;
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN xmin OPTIONS (ADD p1 'v1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
+                         ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ \d+ ft1
  -- can't change the column type if it's used elsewhere
  CREATE TABLE use_ft1_column_type (x ft1);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE integer;	-- ERROR
#9Robert Haas
robertmhaas@gmail.com
In reply to: Shigeru Hanada (#8)
Re: per-column generic option

2011/6/27 Shigeru Hanada <shigeru.hanada@gmail.com>:

* It might be an option to extend attreloptions, instead of the new
attfdwoptions.
Although I didn't track the discussion when pg_foreign_table catalog
that provides
relation level fdw-options, was it impossible or unreasonable to extend existing
design of reloptions/attoptions?
Right now, it accepts only hard-wired options listed at reloptions.c.
But, it seems
to me worthwhile, if it could accept options validated by loadable modules.

IIRC someone has objected against storing FDW options in
reloptions/attoptions, but I couldn't find such post.  I'll follow the
discussion again.

I think they should definitely be separate.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#10David Fetter
david@fetter.org
In reply to: David Fetter (#6)
Re: per-column generic option

On Fri, Jun 17, 2011 at 05:59:31AM -0700, David Fetter wrote:

On Fri, Jun 17, 2011 at 07:19:39PM +0900, Shigeru Hanada wrote:

Here's an example of a non-trivial mapping.

Database type:
MySQL
Foreign data type:
datetime
PostgreSQL data type:
timestamptz
Transformation direction:
Import
Transformation:
CASE
WHEN DATA = '0000-00-00 00:00:00'
THEN NULL
ELSE DATA
END

Here, I'm making the simplifying assumption that there is a bijective
mapping between data types.

Any word on this?

Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#11Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: David Fetter (#6)
Re: per-column generic option

Sorry for the long delay...

(2011/06/17 21:59), David Fetter wrote:

On Fri, Jun 17, 2011 at 07:19:39PM +0900, Shigeru Hanada wrote:

(2011/06/17 8:44), David Fetter wrote:

Sorry not to respond sooner.

First, the per-column generic options are a great thing for us to
have. :)

Thanks for the comments. :-)

I have an idea I've been using for the next release of DBI-Link that
has varying levels of data type mapping. In general, these mappings
would be units of executable code, one in-bound, and one out-bound,
for each of:

Universe (everything, default "mapping" is the identity map, i.e. a no-op)
Database type (e.g. MySQL)
Instance (e.g. mysql://foo.bar.com:5432)
Database
Schema
Table
Column

Some of them seem to be able to be mapped to FDW object, e.g. Database
to SERVER and Table to FOREIGN TABLE.

Yes, I see there are a few missing. "Universe" doesn't really need
much of anything, as far as I can tell, except if we wanted to do
something that affected SQL/MED globally. Is that hierarchy otherwise
OK?

Yes, maybe some levels in your hierarchy can be mapped to SQL/MED
objects, and you can store options with them.

Universe : N/A (I'm not sure but custom GUC might suit for this)
Database type : FOREIGN DATA WRAPPER
Instance : N/A
Database : SERVER
Schema : N/A
Table : FOREIGN TABLE
Column : column of FOREIGN TABLE(WIP)

DB2 may have one more level between Instance and Database Type,
that latter being the province of an individual FDW.

I'm not familiar with DB2, but it would be difficult to map such level
to one of existing SQL/MED object types.

I didn't include row in the hierarchy because I couldn't think of a
way to identify rows across DBMSs and stable over time.

The finest-grain transformation that's been set would be the one
actually used.

Yeah, I think it's generally convenient for users if a FDW allows to
override settings which were defined for object on upper level.
For instance, if I'm dealing many files which have same format, and if
we could set "format" option for file_fdw on the server, all I have to
do for each foreign table is to specify "filename". I think that that's
usual use case.

Here's an example of a non-trivial mapping.

Database type:
MySQL
Foreign data type:
datetime
PostgreSQL data type:
timestamptz
Transformation direction:
Import
Transformation:
CASE
WHEN DATA = '0000-00-00 00:00:00'
THEN NULL
ELSE DATA
END

Here, I'm making the simplifying assumption that there is a bijective
mapping between data types.

Is there some way to fit the per-column part of such a mapping into
this scheme? We'd need to do some dependency tracking in order to be
able to point to the appropriate code...

IIUC, you are talking about using FDW options as storage of data
type mapping setting, or mapping definition itself, right? If so, a
foreign table needs to be created to use per-column FDW options.
Does it suit to your idea?

Yes. The only mildly disturbing thing about how that would work is
that "magic" key names would actually point to executable code, so
there would be some kind of non-uniform processing of the options, and
(possibly quite unlikely) ways to escalate privilege.

How are you planning to define a mapping for an object other than
column? ISTM that you need to combine N mappings for such object, N is
the number of distinct types used under the level, so FDW seems to have
to cover all kind of transformation. Maybe you need to retrieve options
from lower level to upper level, until you find one which is suitable
for the combination of types.

BTW, I couldn't get what you mean by "dependency tracking". You
mean the dependency between foreign column and local column? It
might include essence of your idea... Would you explain the detail?

I think the dependency between the mapping between the foreign column
and the local one is already handled. On that subject, it's possible
to make an argument that this mapping might need to be expanded so
that in general, M foreign columns map to N local ones (distinct M and
N), but that's a research topic, so let's not worry about it now.

The dependency tracking I have in mind is of the actual executable
code. If the inbound mapping has what amounts to a pointer to a
function, it shouldn't be possible to drop that function without
CASCADE, and if we're caching such functions, the cache needs to be
refreshed any time the function changes.

Agreed, such dependency would have to be maintained by the system.
Dependencies from column to FDW (through foreign table and server) have
been managed with pg_depend. Cache invalidation would be need to be
implemented by dbi-link.

Current dependency graph about SQL/MED objects is:

column -> foreign table ----> server -> FDW
user mapping _/

VALIDATOR function might be able to be used to maintain pg_depend
entries when options are set/changed/dropped via CREATE/ALTER, though
it's not main purpose of VALIDATOR.

regards,
--
Shigeru Hanada

#12Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Shigeru Hanada (#8)
Re: per-column generic option

Hanada-san,

I checked the per-column generic option patch.
Right now, I have nothing to comment on anymore.
So, it should be reviewed by committers.

Thanks,

2011年6月27日16:47 Shigeru Hanada <shigeru.hanada@gmail.com>:

(2011/06/26 18:34), Kohei KaiGai wrote:

I checked your patch.

Thanks for the review! Please find attached a revised patch.

The backend portion seems to me OK, but I have several questions/comments.

* This patch should be rebased.
It conflicts with the latest bin/psql/describe.c and
include/catalog/catversion.h.
IIRC, we should not touch catversion.h in submission stage. (It might
be a task of
committer when a patch get upstreamed.)

I've rebased against current HEAD, and reverted catversion.h.

* It might be an option to extend attreloptions, instead of the new
attfdwoptions.
Although I didn't track the discussion when pg_foreign_table catalog
that provides
relation level fdw-options, was it impossible or unreasonable to extend existing
design of reloptions/attoptions?
Right now, it accepts only hard-wired options listed at reloptions.c.
But, it seems
to me worthwhile, if it could accept options validated by loadable modules.

IIRC someone has objected against storing FDW options in
reloptions/attoptions, but I couldn't find such post. I'll follow the
discussion again.

IMHO, though at present I don't have clear proof, separating FDW options
from access method options seems better than merging them, but I should
learn more about AM mechanism to clarify this issue. Please check other
issues first.

* pg_dump shall die when we run it for older postgresql version.

This patch does not modify queries to older postgresql version at
getTableAttrs().
In the result, this index shall be set by -1.
+ i_attfdwoptions = PQfnumber(res, "attfdwoptions");

Then, PGgetvalue() returns NULL for unranged column number, and strdup()
shall cause segmentation fault.
+ tbinfo->attfdwoptions[j] = strdup(PQgetvalue(res, j,
i_attfdwoptions));

In fact, I tried to run the patched pg_dump towards v9.0.2
[kaigai@vmlinux pg_dump]$ ./pg_dump postgres
pg_dump: column number -1 is out of range 0..14
Segmentation fault

My recommendation is to append "NULL as attfdwoptions" on the queries to
older versions. It eventually makes PGgetvalue() to return an empty string,
then strdup() does not cause a problem.

Fixed in the way you've recommended, and tested against 8.4. I should
have noticed that same technique is used in some other places...

BTW, I also have found an unnecessary FIXME comment and removed it.
Please see the line 2845 of src/backend/catalog/heap.c
(InsertPgAttributeTuple) for the correction.

Regards,
--
Shigeru Hanada

--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#13Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: Kohei KaiGai (#12)
Re: per-column generic option

(2011/07/03 18:50), Kohei KaiGai wrote:

I checked the per-column generic option patch.
Right now, I have nothing to comment on anymore.
So, it should be reviewed by committers.

Thanks for the review!.

Regards,
--
Shigeru Hanada

#14Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: Shigeru Hanada (#13)
1 attachment(s)
Re: per-column generic option

(2011/07/04 10:17), Shigeru Hanada wrote:

(2011/07/03 18:50), Kohei KaiGai wrote:

I checked the per-column generic option patch.
Right now, I have nothing to comment on anymore.
So, it should be reviewed by committers.

Thanks for the review!.

I would like to propose adding force_not_null support to file_fdw, as
the first use case of per-column FDW option. Attached patch, which
assumes that per_column_option_v3.patch has been applied, implements
force_not_null option as per-column FDW option.

Overview
========
This option is originally supported by COPY FROM command, so I think
it's reasonable to support it in file_fdw too. It would provides more
flexible parsing capability. In fact, this option has been supported
by the internal routines which are shared with COPY FROM, but currently
we don't have any way to specify it.

Difference between COPY
=======================
For COPY FROM, FORCE_NOT_NULL is specified as a list of column names
('*' is not allowed). For file_fdw, per-table FDW option can be used
like other options, but then file_fdw needs parser which can identify
valid column. I think it's too much work, so I prefer per-column FDW
option which accepts boolean value string. The value 'true' means that
the column doesn't be matched against NULL string, same as ones listed
for COPY FROM.

Example:

If you have created a foreign table with:

CREATE FOREIGN TABLE foo (
c1 int OPTIONS (force_not_null 'false'),
c2 text OPTIONS (force_not_null 'true')
) SERVER file OPTIONS (file '/path/to/file', format 'csv', null '');

values which are read from the file for c1 are matched against
null-representation-string '', but values for c2 are NOT. Empty strings
for c2 are stored as empty strings; they don't treated as NULL value.

Regards,
--
Shigeru Hanada

Attachments:

force_not_null_v1.patchtext/plain; name=force_not_null_v1.patchDownload
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 466c015..caf9c86 100644
*** a/contrib/file_fdw/file_fdw.c
--- b/contrib/file_fdw/file_fdw.c
***************
*** 23,29 ****
--- 23,31 ----
  #include "foreign/fdwapi.h"
  #include "foreign/foreign.h"
  #include "miscadmin.h"
+ #include "nodes/makefuncs.h"
  #include "optimizer/cost.h"
+ #include "utils/syscache.h"
  
  PG_MODULE_MAGIC;
  
*************** static struct FileFdwOption valid_option
*** 56,71 ****
  	{"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}
--- 58,69 ----
  	{"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
*** 111,116 ****
--- 109,115 ----
  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)
*** 144,149 ****
--- 143,149 ----
  	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)
*** 197,203 ****
  							 buf.data)));
  		}
  
! 		/* Separate out filename, since ProcessCopyOptions won't allow it */
  		if (strcmp(def->defname, "filename") == 0)
  		{
  			if (filename)
--- 197,206 ----
  							 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)
*** 206,211 ****
--- 209,228 ----
  						 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 
*** 236,245 ****
--- 253,347 ----
  }
  
  /*
+  * 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))
+ 			ereport(ERROR,
+ 					(errcode(ERRCODE_UNDEFINED_OBJECT),
+ 					 errmsg("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 DefElemn 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,
*** 286,291 ****
--- 388,398 ----
  		}
  		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 filename is required optiono. */
  	if (*filename == NULL)
  		ereport(ERROR,
  				(errcode(ERRCODE_FDW_UNABLE_TO_CREATE_REPLY),
diff --git a/contrib/file_fdw/input/file_fdw.source b/contrib/file_fdw/input/file_fdw.source
index 9ff7235..51e8ff0 100644
*** a/contrib/file_fdw/input/file_fdw.source
--- b/contrib/file_fdw/input/file_fdw.source
*************** CREATE FOREIGN TABLE agg_bad (
*** 77,82 ****
--- 77,96 ----
  ) 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 2ba36c9..e4c4700 100644
*** a/contrib/file_fdw/output/file_fdw.source
--- b/contrib/file_fdw/output/file_fdw.source
*************** CREATE FOREIGN TABLE agg_bad (
*** 91,96 ****
--- 91,126 ----
  	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;
*** 214,220 ****
  -- 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
--- 244,250 ----
  -- 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
*** 222,225 ****
--- 252,256 ----
  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..776aa9a 100644
*** a/doc/src/sgml/file-fdw.sgml
--- b/doc/src/sgml/file-fdw.sgml
***************
*** 111,124 ****
   </variablelist>
  
   <para>
!   <command>COPY</>'s <literal>OIDS</literal>, <literal>FORCE_QUOTE</literal>,
!   and <literal>FORCE_NOT_NULL</literal> options are currently not supported by
    <literal>file_fdw</>.
   </para>
  
   <para>
!   These options can only be specified for a foreign table, not in the
!   options of the <literal>file_fdw</> foreign-data wrapper, nor in the
    options of a server or user mapping using the wrapper.
   </para>
  
--- 111,147 ----
   </variablelist>
  
   <para>
!   A column of a foreign table created using this wrapper can have the
!   following options:
!  </para>
! 
!  <variablelist>
! 
!   <varlistentry>
!    <term><literal>force_not_null</literal></term>
! 
!    <listitem>
!     <para>
!      Specifies whether values for the column shouldn't been matched against
!      the null string.  Acceptable values are <literal>true</> for no matching,
!      and <literal>false</> for matching (case sensitive).
!      <literal>true</> is same as specifing the column in <command>COPY</>'s
!      <literal>FORCE_NOT_NULL</literal> option.
!     </para>
!    </listitem>
!   </varlistentry>
! 
!  </variablelist>
! 
!  <para>
!   <command>COPY</>'s <literal>OIDS</literal> and
!   <literal>FORCE_QUOTE</literal> options are currently not supported by
    <literal>file_fdw</>.
   </para>
  
   <para>
!   These options can only be specified for a foreign table or its columns, not
!   in the options of the <literal>file_fdw</> foreign-data wrapper, nor in the
    options of a server or user mapping using the wrapper.
   </para>
  
#15Alvaro Herrera
alvherre@commandprompt.com
In reply to: Shigeru Hanada (#8)
Re: per-column generic option

Shigeru Hanada escribió:

(2011/06/26 18:34), Kohei KaiGai wrote:

I checked your patch.

Thanks for the review! Please find attached a revised patch.

Err, \dec seems to have a line in describe.h but nowhere else; are you
going to introduce that command?

The new ALTER TABLE grammar seems a bit strange -- ADD, SET, DROP. Is
this defined by the SQL/MED standard? It seems at odds with our
handling of attoptions (and the pg_dump query seems rather bizarre in
comparison to the handling of attoptions there; why do we need
pg_options_to_table when attoptions do not?).

What's the reason for this:

@@ -3681,7 +3691,7 @@ AlterFdwStmt: ALTER FOREIGN DATA_P WRAPPER name opt_fdw_options alter_generic_op
 /* Options definition for CREATE FDW, SERVER and USER MAPPING */
 create_generic_options:
            OPTIONS '(' generic_option_list ')'         { $$ = $3; }
-           | /*EMPTY*/                                 { $$ = NIL; }
+           | /*EMPTY*/                                 { $$ = NIL }
        ;

I think this should be removed:

+       foreach (clist, column->fdwoptions)
+       {
+           DefElem        *option = (DefElem *) lfirst(clist);
+           elog(DEBUG3, "%s=%s", option->defname, strVal(option->arg));
+       }

As for whether attfdwoptions needs to be separate from attoptions, I am
not sure that they really need to be; but if they are, I think we should
use a different name than attfdwoptions, because we might want to use
them for something else. Maybe attgenoptions (and note that the alter
table code already calls them "generic options" so I'm not sure why the
rest of the code is calling them FDW options) ... but then I really
start to question whether they need to be separate from attoptions.

Currently, attoptions are used to store n_distinct and
n_distinct_inherited. Do those options make sense for foreign tables?
If they do make sense for some types of foreign servers, maybe we should
decree that they need to be specifically declared as such by the FDW --
that is, the FDW needs to provide its own attribute_reloptions routine
(or equivalent therein) for validation that includes those core options.
There is no saying that, even if all existing core options such as
n_distinct apply to a FDW now, more core options that we might invent in
the future are going to apply as well.

In short: in my opinion, attoptions and attfdwoptions need to be one
thing and the same.

--
Álvaro Herrera <alvherre@commandprompt.com>
The PostgreSQL Company - Command Prompt, Inc.
PostgreSQL Replication, Consulting, Custom Development, 24x7 support

#16Robert Haas
robertmhaas@gmail.com
In reply to: Alvaro Herrera (#15)
Re: per-column generic option

On Jul 9, 2011, at 10:49 PM, Alvaro Herrera <alvherre@commandprompt.com> wrote:

In short: in my opinion, attoptions and attfdwoptions need to be one
thing and the same.

I feel the opposite. In particular, what happens when a future release of PostgreSQL adds an attoption that happens to have the same name as somebody's per-column FDW option? Something breaks, that's what...

Another point: We don't commingle these concepts at the table level. It doesn't make sense to have table reloptions separate from table FDW options but then go and make the opposite decision at the column level.

...Robert

#17Alvaro Herrera
alvherre@commandprompt.com
In reply to: Robert Haas (#16)
Re: per-column generic option

Excerpts from Robert Haas's message of dom jul 10 21:21:19 -0400 2011:

On Jul 9, 2011, at 10:49 PM, Alvaro Herrera <alvherre@commandprompt.com> wrote:

In short: in my opinion, attoptions and attfdwoptions need to be one
thing and the same.

I feel the opposite. In particular, what happens when a future release of PostgreSQL adds an attoption that happens to have the same name as somebody's per-column FDW option? Something breaks, that's what...

Hmm, if you follow my proposal above, that wouldn't actually happen,
because the core options do not apply to foreign columns.

Another point: We don't commingle these concepts at the table level.
It doesn't make sense to have table reloptions separate from table FDW
options but then go and make the opposite decision at the column
level.

That's a point. I remember feeling uneasy at the fact that we were
doing things like that, at the time, yes :-)

--
Álvaro Herrera <alvherre@commandprompt.com>
The PostgreSQL Company - Command Prompt, Inc.
PostgreSQL Replication, Consulting, Custom Development, 24x7 support

#18Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: Alvaro Herrera (#15)
1 attachment(s)
Re: per-column generic option

Thanks for the review.

(2011/07/10 12:49), Alvaro Herrera wrote:

Err, \dec seems to have a line in describe.h but nowhere else; are you
going to introduce that command?

\dec command is obsolete, so I removed that line.

The new ALTER TABLE grammar seems a bit strange -- ADD, SET, DROP. Is
this defined by the SQL/MED standard?

Yes, syntax for altering foreign table is defined by the SQL/MED
standard as below, and <alter generic option> is common to all SQL/MED
objects:

<alter foreign table statement> ::=
ALTER FOREIGN TABLE <table name> <alter foreign table action>

<alter foreign table action> ::=
<add basic column definition>
| <alter basic column definition>
| <drop basic column definition>
| <alter generic options>

<alter generic options> ::=
OPTIONS <left paren> <alter generic option list> <right paren>

<alter generic option list> ::=
<alter generic option> [ { <comma> <alter generic option> }... ]

<alter generic option> ::= [ <alter operation> ] <option name> [ <option
value> ]
<alter operation> ::=
ADD
| SET
| DROP

<generic option> ::= <option name> [ <option value> ]

<option value> ::= <character string literal>

FYI, default for <alter operation> is ADD.

It seems at odds with our
handling of attoptions (and the pg_dump query seems rather bizarre in
comparison to the handling of attoptions there; why do we need
pg_options_to_table when attoptions do not?).

That's because of the syntax difference between FDW options and AM
options. AM options should be dumped as "key=value, key=value, ...",
but FDW options should be dumped as "key 'value', key 'value', ...". The
pg_options_to_table() is used to construct list in the latter form.
The way used to handle per-column options in my patch is same as the way
used for other existing FDW objects, such as FDW, server, and user mapping.

What's the reason for this:

@@ -3681,7 +3691,7 @@ AlterFdwStmt: ALTER FOREIGN DATA_P WRAPPER name opt_fdw_options alter_generic_op
/* Options definition for CREATE FDW, SERVER and USER MAPPING */
create_generic_options:
OPTIONS '(' generic_option_list ')'         { $$ = $3; }
-           | /*EMPTY*/                                 { $$ = NIL; }
+           | /*EMPTY*/                                 { $$ = NIL }
;

Reverted this unintended change.

I think this should be removed:

+       foreach (clist, column->fdwoptions)
+       {
+           DefElem        *option = (DefElem *) lfirst(clist);
+           elog(DEBUG3, "%s=%s", option->defname, strVal(option->arg));
+       }

Removed, the codes were used only for debug.

As for whether attfdwoptions needs to be separate from attoptions, I am
not sure that they really need to be; but if they are, I think we should
use a different name than attfdwoptions, because we might want to use
them for something else. Maybe attgenoptions (and note that the alter
table code already calls them "generic options" so I'm not sure why the
rest of the code is calling them FDW options) ... but then I really
start to question whether they need to be separate from attoptions.

For now I got +1 for attfdwoptions and +1 for attgenoptions for the
naming. I prefer attgenoptions because it follows SQL/MED standard, but
I don't have strong feeling for naming, so I've inspected usage in the
current HEAD... Hm, "gen.*option" appears twice much as "fdw.*option"
in the source code with case insensitive grep, and most of "fdw.*option"
were hit "fdwoptions", per-wrapper options. ISTM that "generic option"
would be better to mean options used by FDW for consistency, so I
unified the wording to "generic option" from "fdw option". I hope that
the name "generic option" doesn't confuse users who aren't familiar to
SQL/MED standard.

Currently, attoptions are used to store n_distinct and
n_distinct_inherited. Do those options make sense for foreign tables?
If they do make sense for some types of foreign servers, maybe we should
decree that they need to be specifically declared as such by the FDW --
that is, the FDW needs to provide its own attribute_reloptions routine
(or equivalent therein) for validation that includes those core options.
There is no saying that, even if all existing core options such as
n_distinct apply to a FDW now, more core options that we might invent in
the future are going to apply as well.

In short: in my opinion, attoptions and attfdwoptions need to be one
thing and the same.

The n_distinct might make sense for every foreign tables in a sense,
though syntax to set it is not supported. It would allow users to
specify not-FDW-specific statistics information to control costs for the
scan. However each FDW would be able to support such option too. I
think that reloptions and attoptions should be used by only PG core, and
FDW should use generic options. So I prefer separated design.

The attached patch fixes issues other than generic options separation.

Regards,
--
Shigeru Hanada

Attachments:

per_column_option_v4.patchtext/plain; name=per_column_option_v4.patchDownload
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index d4a1d36..3b56bbf 100644
*** a/doc/src/sgml/catalogs.sgml
--- b/doc/src/sgml/catalogs.sgml
***************
*** 1152,1157 ****
--- 1152,1166 ----
        </entry>
       </row>
  
+      <row>
+       <entry><structfield>attgenoptions</structfield></entry>
+       <entry><type>text[]</type></entry>
+       <entry></entry>
+       <entry>
+        Attribute-level generic options, as <quote>keyword=value</> strings
+       </entry>
+      </row>
+ 
      </tbody>
     </tgroup>
    </table>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 77a00cd..1a89b4e 100644
*** a/doc/src/sgml/information_schema.sgml
--- b/doc/src/sgml/information_schema.sgml
***************
*** 1005,1010 ****
--- 1005,1073 ----
    </table>
   </sect1>
  
+  <sect1 id="infoschema-column-options">
+   <title><literal>column_options</literal></title>
+ 
+   <para>
+    The view <literal>column_options</literal> contains all the
+    options defined for foreign table columns in the current database.  Only
+    those foreign table columns are shown that the current user has access to
+    (by way of being the owner or having some privilege).
+   </para>
+ 
+   <table>
+    <title><literal>column_options</literal> Columns</title>
+ 
+    <tgroup cols="3">
+     <thead>
+      <row>
+       <entry>Name</entry>
+       <entry>Data Type</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+ 
+     <tbody>
+      <row>
+       <entry><literal>table_catalog</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the database that contains the foreign table (always the current database)</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>table_schema</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the schema that contains the foreign table</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>table_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the foreign table</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>column_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of the column</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>option_name</literal></entry>
+       <entry><type>sql_identifier</type></entry>
+       <entry>Name of an option</entry>
+      </row>
+ 
+      <row>
+       <entry><literal>option_value</literal></entry>
+       <entry><type>character_data</type></entry>
+       <entry>Value of the option</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect1>
+ 
   <sect1 id="infoschema-column-privileges">
    <title><literal>column_privileges</literal></title>
  
diff --git a/doc/src/sgml/ref/alter_foreign_table.sgml b/doc/src/sgml/ref/alter_foreign_table.sgml
index a45df02..95ae02a 100644
*** a/doc/src/sgml/ref/alter_foreign_table.sgml
--- b/doc/src/sgml/ref/alter_foreign_table.sgml
*************** ALTER FOREIGN TABLE <replaceable class="
*** 36,41 ****
--- 36,42 ----
      DROP [ COLUMN ] [ IF EXISTS ] <replaceable class="PARAMETER">column</replaceable> [ RESTRICT | CASCADE ]
      ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> [ SET DATA ] TYPE <replaceable class="PARAMETER">type</replaceable>
      ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> { SET | DROP } NOT NULL
+     ALTER [ COLUMN ] <replaceable class="PARAMETER">column</replaceable> OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
      OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
      OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ])
  </synopsis>
*************** ALTER FOREIGN TABLE <replaceable class="
*** 125,136 ****
      <term><literal>OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ] )</literal></term>
      <listitem>
       <para>
!       Change options for the foreign table.
        <literal>ADD</>, <literal>SET</>, and <literal>DROP</>
        specify the action to be performed.  <literal>ADD</> is assumed
!       if no operation is explicitly specified.  Option names must be
!       unique; names and values are also validated using the foreign
!       data wrapper library.
       </para>
      </listitem>
     </varlistentry>
--- 126,137 ----
      <term><literal>OPTIONS ( [ ADD | SET | DROP ] <replaceable class="PARAMETER">option</replaceable> ['<replaceable class="PARAMETER">value</replaceable>'] [, ... ] )</literal></term>
      <listitem>
       <para>
!       Change options for the foreign table or the column of the foreign table.
        <literal>ADD</>, <literal>SET</>, and <literal>DROP</>
        specify the action to be performed.  <literal>ADD</> is assumed
!       if no operation is explicitly specified.  Option names must be unique
!       in each associated object; names and values are also validated using the
!       foreign data wrapper library.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml
index ad91072..8863386 100644
*** a/doc/src/sgml/ref/create_foreign_table.sgml
--- b/doc/src/sgml/ref/create_foreign_table.sgml
***************
*** 19,25 ****
   <refsynopsisdiv>
  <synopsis>
  CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="PARAMETER">table_name</replaceable> ( [
!   { <replaceable class="PARAMETER">column_name</replaceable> <replaceable class="PARAMETER">data_type</replaceable> [ NULL | NOT NULL ] }
      [, ... ]
  ] )
    SERVER <replaceable class="parameter">server_name</replaceable>
--- 19,25 ----
   <refsynopsisdiv>
  <synopsis>
  CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="PARAMETER">table_name</replaceable> ( [
!   { <replaceable class="PARAMETER">column_name</replaceable> <replaceable class="PARAMETER">data_type</replaceable> [ OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ... ] ) ] [ NULL | NOT NULL ] }
      [, ... ]
  ] )
    SERVER <replaceable class="parameter">server_name</replaceable>
*************** CREATE FOREIGN TABLE [ IF NOT EXISTS ] <
*** 138,147 ****
      <term><literal>OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ...] )</literal></term>
      <listitem>
       <para>
!       Options to be associated with the new foreign table.
        The allowed option names and values are specific to each foreign
        data wrapper and are validated using the foreign-data wrapper's
!       validator function. Option names must be unique.
       </para>
      </listitem>
     </varlistentry>
--- 138,149 ----
      <term><literal>OPTIONS ( <replaceable class="PARAMETER">option</replaceable> '<replaceable class="PARAMETER">value</replaceable>' [, ...] )</literal></term>
      <listitem>
       <para>
!       Options to be associated with the new foreign table or the column of
!       the foreign table.
        The allowed option names and values are specific to each foreign
        data wrapper and are validated using the foreign-data wrapper's
!       validator function. Option names must be unique in each associated
!       object.
       </para>
      </listitem>
     </varlistentry>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 6385c78..1b48311 100644
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** testdb=&gt;
*** 891,896 ****
--- 891,903 ----
          below.)
          </para>
  
+ 		<para>
+         In addition to common information, <literal>\d</> shows
+         relation-kind-specific information for each column:
+         column values for sequences, indexed expression for indexes and
+         per-column generic options for foreign tables.
+ 		</para>
+ 
          <para>
          The command form <literal>\d+</literal> is identical, except that
          more information is displayed: any comments associated with the
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 4656dba..4333207 100644
*** a/src/backend/access/common/tupdesc.c
--- b/src/backend/access/common/tupdesc.c
*************** equalTupleDescs(TupleDesc tupdesc1, Tupl
*** 363,369 ****
  			return false;
  		if (attr1->attcollation != attr2->attcollation)
  			return false;
! 		/* attacl and attoptions are not even present... */
  	}
  
  	if (tupdesc1->constr != NULL)
--- 363,369 ----
  			return false;
  		if (attr1->attcollation != attr2->attcollation)
  			return false;
! 		/* attacl, attoptions and attgenoptions are not even present... */
  	}
  
  	if (tupdesc1->constr != NULL)
*************** TupleDescInitEntry(TupleDesc desc,
*** 483,489 ****
  	att->attisdropped = false;
  	att->attislocal = true;
  	att->attinhcount = 0;
! 	/* attacl and attoptions are not present in tupledescs */
  
  	tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(oidtypeid));
  	if (!HeapTupleIsValid(tuple))
--- 483,489 ----
  	att->attisdropped = false;
  	att->attislocal = true;
  	att->attinhcount = 0;
! 	/* attacl, attoptions and attgenoptions are not present in tupledescs */
  
  	tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(oidtypeid));
  	if (!HeapTupleIsValid(tuple))
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index 0aeaf5b..0a72eb4 100644
*** a/src/backend/catalog/genbki.pl
--- b/src/backend/catalog/genbki.pl
*************** sub emit_pgattr_row
*** 369,375 ****
          attislocal    => 't',
          attinhcount   => '0',
          attacl        => '_null_',
!         attoptions    => '_null_'
      );
      return {%PGATTR_DEFAULTS, %row};
  }
--- 369,376 ----
          attislocal    => 't',
          attinhcount   => '0',
          attacl        => '_null_',
!         attoptions    => '_null_',
!         attgenoptions => '_null_'
      );
      return {%PGATTR_DEFAULTS, %row};
  }
*************** sub emit_schemapg_row
*** 400,405 ****
--- 401,407 ----
      # Only the fixed-size portions of the descriptors are ever used.
      delete $row->{attacl};
      delete $row->{attoptions};
+     delete $row->{attgenoptions};
  
      # Expand booleans from 'f'/'t' to 'false'/'true'.
      # Some values might be other macros (eg FLOAT4PASSBYVAL), don't change.
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index a8c2700..bc161a4 100644
*** a/src/backend/catalog/heap.c
--- b/src/backend/catalog/heap.c
*************** static List *insert_ordered_unique_oid(L
*** 126,132 ****
   */
  
  /*
!  * The initializers below do not include the attoptions or attacl fields,
   * but that's OK - we're never going to reference anything beyond the
   * fixed-size portion of the structure anyway.
   */
--- 126,132 ----
   */
  
  /*
!  * The initializers below do not include trailing variable length fields,
   * but that's OK - we're never going to reference anything beyond the
   * fixed-size portion of the structure anyway.
   */
*************** InsertPgAttributeTuple(Relation pg_attri
*** 607,612 ****
--- 607,613 ----
  	/* start out with empty permissions and empty options */
  	nulls[Anum_pg_attribute_attacl - 1] = true;
  	nulls[Anum_pg_attribute_attoptions - 1] = true;
+ 	nulls[Anum_pg_attribute_attgenoptions - 1] = true;
  
  	tup = heap_form_tuple(RelationGetDescr(pg_attribute_rel), values, nulls);
  
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index 966eb08..372a03d 100644
*** a/src/backend/catalog/information_schema.sql
--- b/src/backend/catalog/information_schema.sql
*************** GRANT SELECT ON element_types TO PUBLIC;
*** 2517,2522 ****
--- 2517,2555 ----
  
  -- SQL/MED views; these use section numbers from part 9 of the standard.
  
+ /* Base view for foreign table columns */
+ CREATE VIEW _pg_foreign_table_columns AS
+     SELECT n.nspname,
+            c.relname,
+            a.attname,
+            a.attgenoptions
+     FROM pg_foreign_table t, pg_authid u, pg_namespace n, pg_class c,
+          pg_attribute a
+     WHERE u.oid = c.relowner
+           AND (pg_has_role(c.relowner, 'USAGE')
+                OR has_column_privilege(c.oid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'))
+           AND n.oid = c.relnamespace
+           AND c.oid = t.ftrelid
+           AND c.relkind = 'f'
+           AND a.attrelid = c.oid
+           AND a.attnum > 0;
+ 
+ /*
+  * 24.2
+  * COLUMN_OPTIONS view
+  */
+ CREATE VIEW column_options AS
+     SELECT CAST(current_database() AS sql_identifier) AS table_catalog,
+            c.nspname AS table_schema,
+            c.relname AS table_name,
+            c.attname AS column_name,
+            CAST((pg_options_to_table(c.attgenoptions)).option_name AS sql_identifier) AS option_name,
+            CAST((pg_options_to_table(c.attgenoptions)).option_value AS character_data) AS option_value
+     FROM _pg_foreign_table_columns c;
+ 
+ GRANT SELECT ON column_options TO PUBLIC;
+ 
+ 
  /* Base view for foreign-data wrappers */
  CREATE VIEW _pg_foreign_data_wrappers AS
      SELECT w.oid,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 295a1ff..425cd3a 100644
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** static void ATPrepAlterColumnType(List *
*** 346,351 ****
--- 346,352 ----
  static bool ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno);
  static void ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
  					  AlterTableCmd *cmd, LOCKMODE lockmode);
+ static void ATExecAlterColumnGenericOptions(Relation rel, const char *colName, List *options, LOCKMODE lockmode);
  static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode);
  static void ATPostAlterTypeParse(char *cmd, List **wqueue, LOCKMODE lockmode);
  static void change_owner_recurse_to_sequences(Oid relationOid,
*************** AlterTableGetLockLevel(List *cmds)
*** 2646,2651 ****
--- 2647,2653 ----
  			case AT_DropNotNull:		/* may change some SQL plans */
  			case AT_SetNotNull:
  			case AT_GenericOptions:
+ 			case AT_AlterColumnGenericOptions:
  				cmd_lockmode = AccessExclusiveLock;
  				break;
  
*************** ATPrepCmd(List **wqueue, Relation rel, A
*** 2923,2928 ****
--- 2925,2936 ----
  			ATPrepAlterColumnType(wqueue, tab, rel, recurse, recursing, cmd, lockmode);
  			pass = AT_PASS_ALTER_TYPE;
  			break;
+ 		case AT_AlterColumnGenericOptions:
+ 			ATSimplePermissions(rel, ATT_FOREIGN_TABLE);
+ 			/* This command never recurses */
+ 			/* No command-specific prep needed */
+ 			pass = AT_PASS_MISC;
+ 			break;
  		case AT_ChangeOwner:	/* ALTER OWNER */
  			/* This command never recurses */
  			/* No command-specific prep needed */
*************** ATExecCmd(List **wqueue, AlteredTableInf
*** 3167,3172 ****
--- 3175,3183 ----
  		case AT_AlterColumnType:		/* ALTER COLUMN TYPE */
  			ATExecAlterColumnType(tab, rel, cmd, lockmode);
  			break;
+ 		case AT_AlterColumnGenericOptions:	/* ALTER COLUMN OPTIONS */
+ 			ATExecAlterColumnGenericOptions(rel, cmd->name, (List *) cmd->def, lockmode);
+ 			break;
  		case AT_ChangeOwner:	/* ALTER OWNER */
  			ATExecChangeOwner(RelationGetRelid(rel),
  							  get_role_oid(cmd->name, false),
*************** ATExecAlterColumnType(AlteredTableInfo *
*** 7380,7385 ****
--- 7391,7490 ----
  	heap_freetuple(heapTup);
  }
  
+ static void
+ ATExecAlterColumnGenericOptions(Relation rel,
+ 								const char *colName,
+ 								List *options,
+ 								LOCKMODE lockmode)
+ {
+ 	Relation	ftrel;
+ 	Relation	attrel;
+ 	ForeignServer *server;
+ 	ForeignDataWrapper *fdw;
+ 	HeapTuple	tuple;
+ 	HeapTuple	newtuple;
+ 	bool		isnull;
+ 	Datum		repl_val[Natts_pg_attribute];
+ 	bool		repl_null[Natts_pg_attribute];
+ 	bool		repl_repl[Natts_pg_attribute];
+ 	Datum		datum;
+ 	Form_pg_foreign_table fttableform;
+ 	Form_pg_attribute atttableform;
+ 
+ 	if (options == NIL)
+ 		return;
+ 
+ 	/* First, determine FDW validator associated to the foreign table. */
+ 	ftrel = heap_open(ForeignTableRelationId, AccessShareLock);
+ 	tuple = SearchSysCache1(FOREIGNTABLEREL, rel->rd_id);
+ 	if (!HeapTupleIsValid(tuple))
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_UNDEFINED_OBJECT),
+ 				 errmsg("foreign table \"%s\" does not exist",
+ 						RelationGetRelationName(rel))));
+ 	fttableform = (Form_pg_foreign_table) GETSTRUCT(tuple);
+ 	server = GetForeignServer(fttableform->ftserver);
+ 	fdw = GetForeignDataWrapper(server->fdwid);
+ 
+ 	heap_close(ftrel, AccessShareLock);
+ 	ReleaseSysCache(tuple);
+ 
+ 	attrel = heap_open(AttributeRelationId, RowExclusiveLock);
+ 	tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName);
+ 	if (!HeapTupleIsValid(tuple))
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_UNDEFINED_COLUMN),
+ 				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+ 						colName, RelationGetRelationName(rel))));
+ 
+ 	/* Prevent them from altering a system attribute */
+ 	atttableform = (Form_pg_attribute) GETSTRUCT(tuple);
+ 	if (atttableform->attnum <= 0)
+ 		ereport(ERROR,
+ 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ 				 errmsg("cannot alter system column \"%s\"", colName)));
+ 
+ 
+ 	/* Initialize buffers for new tuple values */
+ 	memset(repl_val, 0, sizeof(repl_val));
+ 	memset(repl_null, false, sizeof(repl_null));
+ 	memset(repl_repl, false, sizeof(repl_repl));
+ 
+ 	/* Extract the current options */
+ 	datum = SysCacheGetAttr(ATTNAME,
+ 							tuple,
+ 							Anum_pg_attribute_attgenoptions,
+ 							&isnull);
+ 	if (isnull)
+ 		datum = PointerGetDatum(NULL);
+ 
+ 	/* Transform the options */
+ 	datum = transformGenericOptions(AttributeRelationId,
+ 									datum,
+ 									options,
+ 									fdw->fdwvalidator);
+ 
+ 	if (PointerIsValid(DatumGetPointer(datum)))
+ 		repl_val[Anum_pg_attribute_attgenoptions - 1] = datum;
+ 	else
+ 		repl_null[Anum_pg_attribute_attgenoptions - 1] = true;
+ 
+ 	repl_repl[Anum_pg_attribute_attgenoptions - 1] = true;
+ 
+ 	/* Everything looks good - update the tuple */
+ 
+ 	newtuple = heap_modify_tuple(tuple, RelationGetDescr(attrel),
+ 								 repl_val, repl_null, repl_repl);
+ 	ReleaseSysCache(tuple);
+ 
+ 	simple_heap_update(attrel, &newtuple->t_self, newtuple);
+ 	CatalogUpdateIndexes(attrel, newtuple);
+ 
+ 	heap_close(attrel, RowExclusiveLock);
+ 
+ 	heap_freetuple(newtuple);
+ }
+ 
  /*
   * Cleanup after we've finished all the ALTER TYPE operations for a
   * particular relation.  We have to drop and recreate all the indexes
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c9133dd..f8f5a8a 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyColumnDef(ColumnDef *from)
*** 2312,2317 ****
--- 2312,2318 ----
  	COPY_NODE_FIELD(collClause);
  	COPY_SCALAR_FIELD(collOid);
  	COPY_NODE_FIELD(constraints);
+ 	COPY_NODE_FIELD(genoptions);
  
  	return newnode;
  }
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 681f5f8..81595d3 100644
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outColumnDef(StringInfo str, ColumnDef 
*** 2101,2106 ****
--- 2101,2107 ----
  	WRITE_NODE_FIELD(collClause);
  	WRITE_OID_FIELD(collOid);
  	WRITE_NODE_FIELD(constraints);
+ 	WRITE_NODE_FIELD(genoptions);
  }
  
  static void
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7226032..99b513f 100644
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** alter_table_cmd:
*** 1769,1774 ****
--- 1769,1783 ----
  					def->raw_default = $8;
  					$$ = (Node *)n;
  				}
+ 			/* ALTER FOREIGN TABLE <name> ALTER [COLUMN] <colname> OPTIONS */
+ 			| ALTER opt_column ColId alter_generic_options
+ 				{
+ 					AlterTableCmd *n = makeNode(AlterTableCmd);
+ 					n->subtype = AT_AlterColumnGenericOptions;
+ 					n->name = $3;
+ 					n->def = (Node *) $4;
+ 					$$ = (Node *)n;
+ 				}
  			/* ALTER TABLE <name> ADD CONSTRAINT ... */
  			| ADD_P TableConstraint
  				{
*************** TypedTableElement:
*** 2497,2503 ****
  			| TableConstraint					{ $$ = $1; }
  		;
  
! columnDef:	ColId Typename ColQualList
  				{
  					ColumnDef *n = makeNode(ColumnDef);
  					n->colname = $1;
--- 2506,2512 ----
  			| TableConstraint					{ $$ = $1; }
  		;
  
! columnDef:	ColId Typename create_generic_options ColQualList
  				{
  					ColumnDef *n = makeNode(ColumnDef);
  					n->colname = $1;
*************** columnDef:	ColId Typename ColQualList
*** 2510,2516 ****
  					n->raw_default = NULL;
  					n->cooked_default = NULL;
  					n->collOid = InvalidOid;
! 					SplitColQualList($3, &n->constraints, &n->collClause,
  									 yyscanner);
  					$$ = (Node *)n;
  				}
--- 2519,2526 ----
  					n->raw_default = NULL;
  					n->cooked_default = NULL;
  					n->collOid = InvalidOid;
! 					n->genoptions = $3;
! 					SplitColQualList($4, &n->constraints, &n->collClause,
  									 yyscanner);
  					$$ = (Node *)n;
  				}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 1be2ac6..b8bd02d 100644
*** a/src/backend/parser/parse_utilcmd.c
--- b/src/backend/parser/parse_utilcmd.c
*************** transformColumnDefinition(CreateStmtCont
*** 559,564 ****
--- 559,589 ----
  				break;
  		}
  	}
+ 
+ 	/*
+ 	 * Generate ALTER FOREIGN TABLE ALTER COLUMN statement which adds 
+ 	 * per-column generic options for this column.
+ 	 */
+ 	if (column->genoptions != NIL)
+ 	{
+ 		AlterTableStmt *stmt;
+ 		AlterTableCmd  *cmd;
+ 
+ 		cmd = makeNode(AlterTableCmd);
+ 		cmd->subtype = AT_AlterColumnGenericOptions;
+ 		cmd->name = column->colname;
+ 		cmd->def = (Node *) column->genoptions;
+ 		cmd->behavior = DROP_RESTRICT;
+ 		cmd->missing_ok = false;
+ 
+ 		stmt = makeNode(AlterTableStmt);
+ 		stmt->relation = cxt->relation;
+ 		stmt->cmds = NIL;
+ 		stmt->relkind = OBJECT_FOREIGN_TABLE;
+ 		stmt->cmds = lappend(stmt->cmds, cmd);
+ 
+ 		cxt->alist = lappend(cxt->alist, stmt);
+ 	}
  }
  
  /*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 9e69b0f..a785c4a 100644
*** a/src/bin/pg_dump/pg_dump.c
--- b/src/bin/pg_dump/pg_dump.c
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5556,5561 ****
--- 5556,5562 ----
  	int			i_attislocal;
  	int			i_attoptions;
  	int			i_attcollation;
+ 	int			i_attgenoptions;
  	PGresult   *res;
  	int			ntups;
  	bool		hasdefaults;
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5593,5599 ****
  
  		resetPQExpBuffer(q);
  
! 		if (g_fout->remoteVersion >= 90100)
  		{
  			/*
  			 * attcollation is new in 9.1.	Since we only want to dump COLLATE
--- 5594,5624 ----
  
  		resetPQExpBuffer(q);
  
! 		if (g_fout->remoteVersion >= 90200)
! 		{
! 			/*
! 			 * attgenoptions is new in 9.2.
! 			 */
! 			appendPQExpBuffer(q, "SELECT a.attnum, a.attname, a.atttypmod, "
! 							  "a.attstattarget, a.attstorage, t.typstorage, "
! 							  "a.attnotnull, a.atthasdef, a.attisdropped, "
! 							  "a.attlen, a.attalign, a.attislocal, "
! 				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
! 						"array_to_string(a.attoptions, ', ') AS attoptions, "
! 							  "CASE WHEN a.attcollation <> t.typcollation "
! 							"THEN a.attcollation ELSE 0 END AS attcollation, "
! 				  "array_to_string(ARRAY("
! 				  "  SELECT option_name || ' ' || quote_literal(option_value) "
! 				  "  FROM pg_options_to_table(attgenoptions)), ', ') "
! 				  " AS attgenoptions "
! 			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
! 							  "ON a.atttypid = t.oid "
! 							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
! 							  "AND a.attnum > 0::pg_catalog.int2 "
! 							  "ORDER BY a.attrelid, a.attnum",
! 							  tbinfo->dobj.catId.oid);
! 		}
! 		else if (g_fout->remoteVersion >= 90100)
  		{
  			/*
  			 * attcollation is new in 9.1.	Since we only want to dump COLLATE
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5608,5614 ****
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
  						"array_to_string(a.attoptions, ', ') AS attoptions, "
  							  "CASE WHEN a.attcollation <> t.typcollation "
! 							"THEN a.attcollation ELSE 0 END AS attcollation "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
--- 5633,5640 ----
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
  						"array_to_string(a.attoptions, ', ') AS attoptions, "
  							  "CASE WHEN a.attcollation <> t.typcollation "
! 							"THEN a.attcollation ELSE 0 END AS attcollation, "
! 							  "NULL AS attgenoptions "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5616,5621 ****
--- 5642,5648 ----
  							  "ORDER BY a.attrelid, a.attnum",
  							  tbinfo->dobj.catId.oid);
  		}
+ 
  		else if (g_fout->remoteVersion >= 90000)
  		{
  			/* attoptions is new in 9.0 */
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5625,5631 ****
  							  "a.attlen, a.attalign, a.attislocal, "
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
  						"array_to_string(a.attoptions, ', ') AS attoptions, "
! 							  "0 AS attcollation "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
--- 5652,5659 ----
  							  "a.attlen, a.attalign, a.attislocal, "
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
  						"array_to_string(a.attoptions, ', ') AS attoptions, "
! 							  "0 AS attcollation, "
! 							  "NULL AS attgenoptions "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5641,5647 ****
  							  "a.attnotnull, a.atthasdef, a.attisdropped, "
  							  "a.attlen, a.attalign, a.attislocal, "
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
--- 5669,5676 ----
  							  "a.attnotnull, a.atthasdef, a.attisdropped, "
  							  "a.attlen, a.attalign, a.attislocal, "
  				  "pg_catalog.format_type(t.oid,a.atttypmod) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation, "
! 							  "NULL AS attgenoptions "
  			 "FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::pg_catalog.oid "
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5662,5668 ****
  							  "false AS attisdropped, a.attlen, "
  							  "a.attalign, false AS attislocal, "
  							  "format_type(t.oid,a.atttypmod) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation "
  							  "FROM pg_attribute a LEFT JOIN pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::oid "
--- 5691,5698 ----
  							  "false AS attisdropped, a.attlen, "
  							  "a.attalign, false AS attislocal, "
  							  "format_type(t.oid,a.atttypmod) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation, "
! 							  "NULL AS attgenoptions "
  							  "FROM pg_attribute a LEFT JOIN pg_type t "
  							  "ON a.atttypid = t.oid "
  							  "WHERE a.attrelid = '%u'::oid "
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5680,5686 ****
  							  "attlen, attalign, "
  							  "false AS attislocal, "
  							  "(SELECT typname FROM pg_type WHERE oid = atttypid) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation "
  							  "FROM pg_attribute a "
  							  "WHERE attrelid = '%u'::oid "
  							  "AND attnum > 0::int2 "
--- 5710,5717 ----
  							  "attlen, attalign, "
  							  "false AS attislocal, "
  							  "(SELECT typname FROM pg_type WHERE oid = atttypid) AS atttypname, "
! 							  "'' AS attoptions, 0 AS attcollation, "
! 							  "NULL AS attgenoptions "
  							  "FROM pg_attribute a "
  							  "WHERE attrelid = '%u'::oid "
  							  "AND attnum > 0::int2 "
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5708,5713 ****
--- 5739,5745 ----
  		i_attislocal = PQfnumber(res, "attislocal");
  		i_attoptions = PQfnumber(res, "attoptions");
  		i_attcollation = PQfnumber(res, "attcollation");
+ 		i_attgenoptions = PQfnumber(res, "attgenoptions");
  
  		tbinfo->numatts = ntups;
  		tbinfo->attnames = (char **) malloc(ntups * sizeof(char *));
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5724,5729 ****
--- 5756,5762 ----
  		tbinfo->attrdefs = (AttrDefInfo **) malloc(ntups * sizeof(AttrDefInfo *));
  		tbinfo->attoptions = (char **) malloc(ntups * sizeof(char *));
  		tbinfo->attcollation = (Oid *) malloc(ntups * sizeof(Oid));
+ 		tbinfo->attgenoptions = (char **) malloc(ntups * sizeof(char *));
  		tbinfo->inhAttrs = (bool *) malloc(ntups * sizeof(bool));
  		tbinfo->inhAttrDef = (bool *) malloc(ntups * sizeof(bool));
  		tbinfo->inhNotNull = (bool *) malloc(ntups * sizeof(bool));
*************** getTableAttrs(TableInfo *tblinfo, int nu
*** 5750,5755 ****
--- 5783,5789 ----
  			tbinfo->notnull[j] = (PQgetvalue(res, j, i_attnotnull)[0] == 't');
  			tbinfo->attoptions[j] = strdup(PQgetvalue(res, j, i_attoptions));
  			tbinfo->attcollation[j] = atooid(PQgetvalue(res, j, i_attcollation));
+ 			tbinfo->attgenoptions[j] = strdup(PQgetvalue(res, j, i_attgenoptions));
  			tbinfo->attrdefs[j] = NULL; /* fix below */
  			if (PQgetvalue(res, j, i_atthasdef)[0] == 't')
  				hasdefaults = true;
*************** dumpTableSchema(Archive *fout, TableInfo
*** 12451,12456 ****
--- 12485,12505 ----
  				appendPQExpBuffer(q, "SET (%s);\n",
  								  tbinfo->attoptions[j]);
  			}
+ 
+ 			/*
+ 			 * Dump per-column generic options.
+ 			 */
+ 			if (tbinfo->relkind == RELKIND_FOREIGN_TABLE &&
+ 				tbinfo->attgenoptions[j] &&
+ 				tbinfo->attgenoptions[j][0] != '\0')
+ 			{
+ 				appendPQExpBuffer(q, "ALTER FOREIGN TABLE %s ",
+ 								  fmtId(tbinfo->dobj.name));
+ 				appendPQExpBuffer(q, "ALTER COLUMN %s ",
+ 								  fmtId(tbinfo->attnames[j]));
+ 				appendPQExpBuffer(q, "OPTIONS (%s);\n",
+ 								  tbinfo->attgenoptions[j]);
+ 			}
  		}
  	}
  
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index c95614b..8d0f25b 100644
*** a/src/bin/pg_dump/pg_dump.h
--- b/src/bin/pg_dump/pg_dump.h
*************** typedef struct _tableInfo
*** 275,280 ****
--- 275,281 ----
  	bool	   *attislocal;		/* true if attr has local definition */
  	char	  **attoptions;		/* per-attribute options */
  	Oid		   *attcollation;	/* per-attribute collation selection */
+ 	char	  **attgenoptions;	/* per-attribute generic options */
  
  	/*
  	 * Note: we need to store per-attribute notnull, default, and constraint
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 682cf8a..428d64a 100644
*** a/src/bin/psql/describe.c
--- b/src/bin/psql/describe.c
*************** describeOneTableDetails(const char *sche
*** 1281,1287 ****
  		res = NULL;
  	}
  
! 	/* Get column info */
  	printfPQExpBuffer(&buf, "SELECT a.attname,");
  	appendPQExpBuffer(&buf, "\n  pg_catalog.format_type(a.atttypid, a.atttypmod),"
  					  "\n  (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)"
--- 1281,1292 ----
  		res = NULL;
  	}
  
! 	/*
! 	 * Get column info
! 	 *
! 	 * You need to modify value of "firstvcol" which willbe defined below if
! 	 * you are adding column(s) preceding to verbose-only columns.
! 	 */
  	printfPQExpBuffer(&buf, "SELECT a.attname,");
  	appendPQExpBuffer(&buf, "\n  pg_catalog.format_type(a.atttypid, a.atttypmod),"
  					  "\n  (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)"
*************** describeOneTableDetails(const char *sche
*** 1295,1300 ****
--- 1300,1311 ----
  		appendPQExpBuffer(&buf, "\n  NULL AS attcollation");
  	if (tableinfo.relkind == 'i')
  		appendPQExpBuffer(&buf, ",\n  pg_catalog.pg_get_indexdef(a.attrelid, a.attnum, TRUE) AS indexdef");
+ 	else
+ 		appendPQExpBuffer(&buf, ",\n  NULL AS indexdef");
+ 	if (tableinfo.relkind == 'f' && pset.sversion >= 90200)
+ 		appendPQExpBuffer(&buf, ",\n  a.attgenoptions");
+ 	else
+ 		appendPQExpBuffer(&buf, ",\n  NULL AS attgenoptions");
  	if (verbose)
  		appendPQExpBuffer(&buf, ",\n  a.attstorage, pg_catalog.col_description(a.attrelid, a.attnum)");
  	appendPQExpBuffer(&buf, "\nFROM pg_catalog.pg_attribute a");
*************** describeOneTableDetails(const char *sche
*** 1376,1381 ****
--- 1387,1395 ----
  	if (tableinfo.relkind == 'i')
  		headers[cols++] = gettext_noop("Definition");
  
+ 	if (tableinfo.relkind == 'f' && pset.sversion >= 90200)
+ 		headers[cols++] = gettext_noop("Options");
+ 
  	if (verbose)
  	{
  		headers[cols++] = gettext_noop("Storage");
*************** describeOneTableDetails(const char *sche
*** 1458,1467 ****
  		if (tableinfo.relkind == 'i')
  			printTableAddCell(&cont, PQgetvalue(res, i, 6), false, false);
  
  		/* Storage and Description */
  		if (verbose)
  		{
! 			int			firstvcol = (tableinfo.relkind == 'i' ? 7 : 6);
  			char	   *storage = PQgetvalue(res, i, firstvcol);
  
  			/* these strings are literal in our syntax, so not translated. */
--- 1472,1485 ----
  		if (tableinfo.relkind == 'i')
  			printTableAddCell(&cont, PQgetvalue(res, i, 6), false, false);
  
+ 		/* Generic options for foreign table column, only for 9.2 or later */
+ 		if (tableinfo.relkind == 'f' && pset.sversion >= 90200)
+ 			printTableAddCell(&cont, PQgetvalue(res, i, 7), false, false);
+ 
  		/* Storage and Description */
  		if (verbose)
  		{
! 			int			firstvcol = 8;
  			char	   *storage = PQgetvalue(res, i, firstvcol);
  
  			/* these strings are literal in our syntax, so not translated. */
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 409d6ea..587c3d6 100644
*** a/src/include/catalog/pg_attribute.h
--- b/src/include/catalog/pg_attribute.h
*************** CATALOG(pg_attribute,1249) BKI_BOOTSTRAP
*** 156,161 ****
--- 156,164 ----
  
  	/* Column-level options */
  	text		attoptions[1];
+ 
+ 	/* Column-level generic options, used by foreign-data wrapper */
+ 	text		attgenoptions[1];
  } FormData_pg_attribute;
  
  /*
*************** typedef FormData_pg_attribute *Form_pg_a
*** 179,185 ****
   * ----------------
   */
  
! #define Natts_pg_attribute				20
  #define Anum_pg_attribute_attrelid		1
  #define Anum_pg_attribute_attname		2
  #define Anum_pg_attribute_atttypid		3
--- 182,188 ----
   * ----------------
   */
  
! #define Natts_pg_attribute				21
  #define Anum_pg_attribute_attrelid		1
  #define Anum_pg_attribute_attname		2
  #define Anum_pg_attribute_atttypid		3
*************** typedef FormData_pg_attribute *Form_pg_a
*** 200,205 ****
--- 203,209 ----
  #define Anum_pg_attribute_attcollation	18
  #define Anum_pg_attribute_attacl		19
  #define Anum_pg_attribute_attoptions	20
+ #define Anum_pg_attribute_attgenoptions	21
  
  
  /* ----------------
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 002ae6b..e006180 100644
*** a/src/include/catalog/pg_class.h
--- b/src/include/catalog/pg_class.h
*************** typedef FormData_pg_class *Form_pg_class
*** 132,138 ****
  /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
  DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
! DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 20 0 f f f f f 3 _null_ _null_ ));
  DESCR("");
  DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 26 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
--- 132,138 ----
  /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
  DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
! DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
  DESCR("");
  DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 26 0 t f f f f 3 _null_ _null_ ));
  DESCR("");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 00c1269..da25787 100644
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct ColumnDef
*** 500,505 ****
--- 500,506 ----
  	CollateClause *collClause;	/* untransformed COLLATE spec, if any */
  	Oid			collOid;		/* collation OID (InvalidOid if not set) */
  	List	   *constraints;	/* other constraints on column */
+ 	List	   *genoptions;		/* per-column generic options */
  } ColumnDef;
  
  /*
*************** typedef enum AlterTableType
*** 1197,1202 ****
--- 1198,1204 ----
  	AT_DropConstraint,			/* drop constraint */
  	AT_DropConstraintRecurse,	/* internal to commands/tablecmds.c */
  	AT_AlterColumnType,			/* alter column type */
+ 	AT_AlterColumnGenericOptions,	/* alter column OPTIONS (...) */
  	AT_ChangeOwner,				/* change owner */
  	AT_ClusterOn,				/* CLUSTER ON */
  	AT_DropCluster,				/* SET WITHOUT CLUSTER */
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index 2b3eddf..45292b5 100644
*** a/src/test/regress/expected/foreign_data.out
--- b/src/test/regress/expected/foreign_data.out
*************** ERROR:  syntax error at or near "WITH OI
*** 646,664 ****
  LINE 1: CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;
                                                ^
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer NOT NULL,
! 	c2 text,
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
  COMMENT ON COLUMN ft1.c1 IS 'ft1.c1';
  \d+ ft1
!               Foreign table "public.ft1"
!  Column |  Type   | Modifiers | Storage  | Description 
! --------+---------+-----------+----------+-------------
!  c1     | integer | not null  | plain    | ft1.c1
!  c2     | text    |           | extended | 
!  c3     | date    |           | plain    | 
  Server: sc
  Has OIDs: no
  
--- 646,664 ----
  LINE 1: CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;
                                                ^
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer OPTIONS (param1 'val1') NOT NULL,
! 	c2 text OPTIONS (param2 'val2', param3 'val3'),
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
  COMMENT ON COLUMN ft1.c1 IS 'ft1.c1';
  \d+ ft1
!                             Foreign table "public.ft1"
!  Column |  Type   | Modifiers |          Options          | Storage  | Description 
! --------+---------+-----------+---------------------------+----------+-------------
!  c1     | integer | not null  | {param1=val1}             | plain    | ft1.c1
!  c2     | text    |           | {param2=val2,param3=val3} | extended | 
!  c3     | date    |           |                           | plain    | 
  Server: sc
  Has OIDs: no
  
*************** ALTER FOREIGN TABLE ft1 ADD COLUMN c6 in
*** 687,693 ****
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer;
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ERROR:  "ft1" is not a table or view
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
--- 687,693 ----
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1');
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ERROR:  "ft1" is not a table or view
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
*************** ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 
*** 698,703 ****
--- 698,724 ----
  ERROR:  "ft1" is not a table
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE text;
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN xmin OPTIONS (ADD p1 'v1'); -- ERROR
+ ERROR:  cannot alter system column "xmin"
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
+                         ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ \d+ ft1
+                             Foreign table "public.ft1"
+  Column |  Type   | Modifiers |          Options          | Storage  | Description 
+ --------+---------+-----------+---------------------------+----------+-------------
+  c1     | integer | not null  | {param1=val1}             | plain    | 
+  c2     | text    |           | {param2=val2,param3=val3} | extended | 
+  c3     | date    |           |                           | plain    | 
+  c4     | integer |           |                           | plain    | 
+  c6     | integer | not null  |                           | plain    | 
+  c7     | integer |           | {p1=v1,p2=v2}             | plain    | 
+  c8     | text    |           | {p2=V2}                   | extended | 
+  c9     | integer |           |                           | plain    | 
+  c10    | integer |           | {p1=v1}                   | plain    | 
+ Server: sc
+ Has OIDs: no
+ 
  -- can't change the column type if it's used elsewhere
  CREATE TABLE use_ft1_column_type (x ft1);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE integer;	-- ERROR
*************** ERROR:  relation "ft1" does not exist
*** 726,742 ****
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME c1 TO foreign_column_1;
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME TO foreign_table_1;
  \d foreign_schema.foreign_table_1
! Foreign table "foreign_schema.foreign_table_1"
!       Column      |  Type   | Modifiers 
! ------------------+---------+-----------
!  foreign_column_1 | integer | not null
!  c2               | text    | 
!  c3               | date    | 
!  c4               | integer | 
!  c6               | integer | not null
!  c7               | integer | 
!  c8               | text    | 
!  c10              | integer | 
  Server: sc
  
  -- Information schema
--- 747,763 ----
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME c1 TO foreign_column_1;
  ALTER FOREIGN TABLE foreign_schema.ft1 RENAME TO foreign_table_1;
  \d foreign_schema.foreign_table_1
!            Foreign table "foreign_schema.foreign_table_1"
!       Column      |  Type   | Modifiers |          Options          
! ------------------+---------+-----------+---------------------------
!  foreign_column_1 | integer | not null  | {param1=val1}
!  c2               | text    |           | {param2=val2,param3=val3}
!  c3               | date    |           | 
!  c4               | integer |           | 
!  c6               | integer | not null  | 
!  c7               | integer |           | {p1=v1,p2=v2}
!  c8               | text    |           | {p2=V2}
!  c10              | integer |           | {p1=v1}
  Server: sc
  
  -- Information schema
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index 58e5060..b3b49cc 100644
*** a/src/test/regress/sql/foreign_data.sql
--- b/src/test/regress/sql/foreign_data.sql
*************** CREATE FOREIGN TABLE ft1 () SERVER no_se
*** 264,271 ****
  CREATE FOREIGN TABLE ft1 (c1 serial) SERVER sc;                 -- ERROR
  CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;                -- ERROR
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer NOT NULL,
! 	c2 text,
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
--- 264,271 ----
  CREATE FOREIGN TABLE ft1 (c1 serial) SERVER sc;                 -- ERROR
  CREATE FOREIGN TABLE ft1 () SERVER sc WITH OIDS;                -- ERROR
  CREATE FOREIGN TABLE ft1 (
! 	c1 integer OPTIONS (param1 'val1') NOT NULL,
! 	c2 text OPTIONS (param2 'val2', param3 'val3'),
  	c3 date
  ) SERVER sc OPTIONS (delimiter ',', quote '"');
  COMMENT ON FOREIGN TABLE ft1 IS 'ft1';
*************** ALTER FOREIGN TABLE ft1 ADD COLUMN c6 in
*** 288,294 ****
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer;
  
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
--- 288,294 ----
  ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer;
  ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer;
! ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1');
  
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0;          -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT;           -- ERROR
*************** ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 
*** 297,302 ****
--- 297,307 ----
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10) USING '0'; -- ERROR
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE char(10);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE text;
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN xmin OPTIONS (ADD p1 'v1'); -- ERROR
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c7 OPTIONS (ADD p1 'v1', ADD p2 'v2'),
+                         ALTER COLUMN c8 OPTIONS (ADD p1 'v1', ADD p2 'v2');
+ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 OPTIONS (SET p2 'V2', DROP p1);
+ \d+ ft1
  -- can't change the column type if it's used elsewhere
  CREATE TABLE use_ft1_column_type (x ft1);
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET DATA TYPE integer;	-- ERROR
#19Peter Eisentraut
peter_e@gmx.net
In reply to: Alvaro Herrera (#15)
Re: per-column generic option

On lör, 2011-07-09 at 23:49 -0400, Alvaro Herrera wrote:

The new ALTER TABLE grammar seems a bit strange -- ADD, SET, DROP. Is
this defined by the SQL/MED standard? It seems at odds with our
handling of attoptions

Well, I believe the SQL/MED options were actually implemented first and
the attoptions afterwards. But it's probably not unwise to keep them
separate, even though the syntaxes could have been made more similar.

#20Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: Robert Haas (#16)
Re: per-column generic option

(2011/07/11 10:21), Robert Haas wrote:

On Jul 9, 2011, at 10:49 PM, Alvaro Herrera<alvherre@commandprompt.com> wrote:

In short: in my opinion, attoptions and attfdwoptions need to be one
thing and the same.

I feel the opposite. In particular, what happens when a future release
of PostgreSQL adds an attoption that happens to have the same name as
somebody's per-column FDW option? Something breaks, that's what...

Another point: We don't commingle these concepts at the table level.
It doesn't make sense to have table reloptions separate from table FDW
options but then go and make the opposite decision at the column
level.

I'm afraid that I've misunderstood the discussion. Do you mean that
per-table options should be stored in reloptions, but per-column should
be separated from attoptions? (I think I've misread...)

Could you tell me little more detail why it doesn't make sense to have
table reloptions separate from table FDW options?

Regards,
--
Shigeru Hanada

#21Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: Peter Eisentraut (#19)
Re: per-column generic option

(2011/07/12 0:44), Peter Eisentraut wrote:

On lör, 2011-07-09 at 23:49 -0400, Alvaro Herrera wrote:

The new ALTER TABLE grammar seems a bit strange -- ADD, SET, DROP. Is
this defined by the SQL/MED standard? It seems at odds with our
handling of attoptions

Well, I believe the SQL/MED options were actually implemented first and
the attoptions afterwards. But it's probably not unwise to keep them
separate, even though the syntaxes could have been made more similar.

As you say, syntax for attoptions/reloptions seem to satisfy the
requirement of SQL/MED; SET for ADD/SET and RESET for DROP.

But at this time it would break backward compatibility. I think it's
reasonable to unify the syntax for handling SQL/MED options at every
level to "OPTIONS (key 'value', ...)".

Regards,
--
Shigeru Hanada

#22Robert Haas
robertmhaas@gmail.com
In reply to: Shigeru Hanada (#20)
Re: per-column generic option

On Jul 12, 2011, at 12:31 AM, Shigeru Hanada <shigeru.hanada@gmail.com> wrote:

(2011/07/11 10:21), Robert Haas wrote:

On Jul 9, 2011, at 10:49 PM, Alvaro Herrera<alvherre@commandprompt.com> wrote:

In short: in my opinion, attoptions and attfdwoptions need to be one
thing and the same.

I feel the opposite. In particular, what happens when a future release
of PostgreSQL adds an attoption that happens to have the same name as
somebody's per-column FDW option? Something breaks, that's what...

Another point: We don't commingle these concepts at the table level.
It doesn't make sense to have table reloptions separate from table FDW
options but then go and make the opposite decision at the column
level.

I'm afraid that I've misunderstood the discussion. Do you mean that
per-table options should be stored in reloptions, but per-column should
be separated from attoptions? (I think I've misread...)

No, I was arguing that they should both be separate.

...Robert

#23Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: Robert Haas (#22)
Re: per-column generic option

(2011/07/12 21:19), Robert Haas wrote:

On Jul 12, 2011, at 12:31 AM, Shigeru Hanada<shigeru.hanada@gmail.com> wrote:

I'm afraid that I've misunderstood the discussion. Do you mean that
per-table options should be stored in reloptions, but per-column should
be separated from attoptions? (I think I've misread...)

No, I was arguing that they should both be separate.

Thanks, I'm relieved. :)

Regards,
--
Shigeru Hanada

#24Alvaro Herrera
alvherre@commandprompt.com
In reply to: Shigeru Hanada (#21)
Re: per-column generic option

Excerpts from Shigeru Hanada's message of mar jul 12 03:11:54 -0400 2011:

(2011/07/12 0:44), Peter Eisentraut wrote:

On lör, 2011-07-09 at 23:49 -0400, Alvaro Herrera wrote:

The new ALTER TABLE grammar seems a bit strange -- ADD, SET, DROP. Is
this defined by the SQL/MED standard? It seems at odds with our
handling of attoptions

Well, I believe the SQL/MED options were actually implemented first and
the attoptions afterwards. But it's probably not unwise to keep them
separate, even though the syntaxes could have been made more similar.

As you say, syntax for attoptions/reloptions seem to satisfy the
requirement of SQL/MED; SET for ADD/SET and RESET for DROP.

Speaking of which -- what's the difference between ADD and SET for SQL/MED
options?

--
Álvaro Herrera <alvherre@commandprompt.com>
The PostgreSQL Company - Command Prompt, Inc.
PostgreSQL Replication, Consulting, Custom Development, 24x7 support

#25Peter Eisentraut
peter_e@gmx.net
In reply to: Alvaro Herrera (#24)
Re: per-column generic option

On tis, 2011-07-12 at 09:56 -0400, Alvaro Herrera wrote:

Speaking of which -- what's the difference between ADD and SET for
SQL/MED options?

ADD add to the existing options, SET overwrites all options with what
you specify.

#26Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: Alvaro Herrera (#24)
Re: per-column generic option

(2011/07/12 22:56), Alvaro Herrera wrote:

Speaking of which -- what's the difference between ADD and SET for SQL/MED
options?

ADD can only add new option; it can't overwrite existing option's value.
To overwrite existing option's value, you need to use SET instead.

Regards,
--
Shigeru Hanada

#27Josh Berkus
josh@agliodbs.com
In reply to: Shigeru Hanada (#26)
Re: per-column generic option

All,

Is the spec for this feature still under discussion? I don't seem to
see a consensus on this thread.

--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com

#28Shigeru Hanada
shigeru.hanada@gmail.com
In reply to: Josh Berkus (#27)
Re: per-column generic option

(2011/07/15 4:17), Josh Berkus wrote:

All,

Is the spec for this feature still under discussion? I don't seem to
see a consensus on this thread.

Yeah, a remaining concern is whether generic (FDW) options should be
separated from existing attoptions or not.

Indeed, reloptions/attoptions mechanism seems to be also applicable to
generic options, since both need to store multiple key-value pairs, but
IMHO generic options should be separated from reloptions/attoptions for
several reasons:

1) FDW options are handled by only FDW, but reloptions/attoptions are
handled by PG core modules such as planner, AM and autovacuum. If we
can separate them completely, they would be able to share one attribute,
but I worry that some of reloptions/attoptions make sense for some FDW.
For instance, n_distinct might be useful to control costs of a foreign
table scan. Though attoptions can't be set via CREATE/ALTER FOREIGN
TABLE yet.

2) In future, newly added option might conflict somebody's FDW option.
Robert Haas has pointed out this issue some days ago. FDW validator
would reject unknown options, so every FDW would have to know all of
reloptions/attoptions to avoid this issue.

3) It would be difficult to unify syntax to set options from perspective
of backward compatibility and syntax consistency. Other SQL/MED objects
have the syntax such as "OPTIONS (key 'value', ...)", but
reloptions/attoptions have the syntax such as "SET (key = value, ...)".
Without syntax unification, some tools should care relkind before
handling attoptions. For instance, pg_dump should choose syntax used to
dump attoptions. It seems undesired complexity.

Any comments/objections/questions are welcome.

Regards,
--
Shigeru Hanada

* 英語 - 自動検出
* 英語
* 日本語

* 英語
* 日本語

<javascript:void(0);>

#29Robert Haas
robertmhaas@gmail.com
In reply to: Alvaro Herrera (#17)
Re: per-column generic option

On Mon, Jul 11, 2011 at 12:11 AM, Alvaro Herrera
<alvherre@commandprompt.com> wrote:

Excerpts from Robert Haas's message of dom jul 10 21:21:19 -0400 2011:

On Jul 9, 2011, at 10:49 PM, Alvaro Herrera <alvherre@commandprompt.com> wrote:

In short: in my opinion, attoptions and attfdwoptions need to be one
thing and the same.

I feel the opposite. In particular, what happens when a future release of PostgreSQL adds an attoption that happens to have the same name as somebody's per-column FDW option?  Something breaks, that's what...

Hmm, if you follow my proposal above, that wouldn't actually happen,
because the core options do not apply to foreign columns.

Well, not at the moment. But I think it's altogether likely that we
might want them to in the future. The foreign data wrapper support we
have right now is basically a stub until we get around to improving
it, so we don't (for example) analyze foreign tables, which means that
n_distinct is not relevant. But that's something we presumably want
to change at some point. Eventually, I would anticipate that we'll
have quite a few more column options and most will apply to both
tables and foreign tables, so I'm not keen to bake in something that
makes that potentially problematic. I think we should understand
attoptions as things that modify the behavior of PostgreSQL, while
attfdw/genoptions are there solely for the foreign data wrapper to
use. An extra nullable field in pg_attribute isn't costing us
anything non-trivial, and the syntactic and definitional clarity seems
entirely worth it.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#30Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#29)
Re: per-column generic option

Robert Haas <robertmhaas@gmail.com> writes:

... I think we should understand
attoptions as things that modify the behavior of PostgreSQL, while
attfdw/genoptions are there solely for the foreign data wrapper to
use. An extra nullable field in pg_attribute isn't costing us
anything non-trivial, and the syntactic and definitional clarity seems
entirely worth it.

+1. We paid the price of allowing nullable columns in pg_attribute long
ago. One more isn't going to cost anything, especially since virtually
every row in that catalog already contains at least one null.

I'm not too thrilled with the terminology of "generic options", though.
I think this should be understood as specifically "FDW-owned options".
If the column isn't reserved for the use of the FDW, then you get right
back into the problem of who's allowed to use it and what if there's a
collision.

regards, tom lane

#31Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#30)
Re: per-column generic option

On Mon, Jul 18, 2011 at 3:26 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

... I think we should understand
attoptions as things that modify the behavior of PostgreSQL, while
attfdw/genoptions are there solely for the foreign data wrapper to
use.  An extra nullable field in pg_attribute isn't costing us
anything non-trivial, and the syntactic and definitional clarity seems
entirely worth it.

+1.  We paid the price of allowing nullable columns in pg_attribute long
ago.  One more isn't going to cost anything, especially since virtually
every row in that catalog already contains at least one null.

I'm not too thrilled with the terminology of "generic options", though.
I think this should be understood as specifically "FDW-owned options".
If the column isn't reserved for the use of the FDW, then you get right
back into the problem of who's allowed to use it and what if there's a
collision.

I concur. The SQL/MED standard is welcome to refer to them as generic
options, but at least FTTB they are going to be entirely for FDWs in
our implementation, and naming them that way is therefore a Good
Thing. If the SQL committee decides to use them in other places and
we choose to support that in some future release for some
as-yet-unclear purpose, well, it won't be the first time we've
modified the system catalog schema.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company