Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add support for modification of OPTIONS of FDW-related objects.

 * docs/foreign.rst: Document new helper class DbObjectWithOptions.
 * pyrseas/dbobject/foreign.py (DbObjectWithOptions): New helper
   class.  ({ForeignDataWrapper, ForeignServer, UserMapping,
   ForeignTable}): Derive from new helper class, add diff_map method
   (except for UserMapping) and change create method.
 * tests/dbobject/test_foreign.py: New tests to verify OPTIONS changes.
  • Loading branch information...
commit 2206633c9907a8474d3725ea9826f9382ab516c6 1 parent c0f146e
@jmafc jmafc authored
View
58 docs/foreign.rst
@@ -3,20 +3,37 @@ Foreign Data Objects
.. module:: pyrseas.dbobject.foreign
-The :mod:`foreign` module defines eight classes: classes
+The :mod:`foreign` module defines nine classes:
+:class:`DbObjectWithOptions` derived from :class:`DbObject`, classes
:class:`ForeignDataWrapper`, :class:`ForeignServer` and
-:class:`UserMapping` derived from :class:`DbObject`,
-:class:`ForeignTable` derived from :class:`Table`, classes
-:class:`ForeignDataWrapperDict`, :class:`ForeignServerDict` and
-:class:`UserMappingDict` derived from :class:`DbObjectDict`, and
-:class:`ForeignTableDict` derived from :class:`ClassDict`.
+:class:`UserMapping` derived from :class:`DbObjectWithOptions`,
+:class:`ForeignTable` derived from :class:`DbObjectWithOptions` and
+:class:`Table`, classes :class:`ForeignDataWrapperDict`,
+:class:`ForeignServerDict` and :class:`UserMappingDict` derived from
+:class:`DbObjectDict`, and :class:`ForeignTableDict` derived from
+:class:`ClassDict`.
+
+Database Object With Options
+----------------------------
+
+:class:`DbObjectWithOptions` is derived from
+:class:`~pyrseas.dbobject.DbObject`. It is a helper function dealing
+with the OPTIONS clauses common to the foreign data objects.
+
+.. autoclass:: DbObjectWithOptions
+
+.. automethod:: DbObjectWithOptions.options_clause
+
+.. automethod:: DbObjectWithOptions.diff_options
+
+.. automethod:: DbObjectWithOptions.diff_map
Foreign Data Wrapper
--------------------
:class:`ForeignDataWrapper` is derived from
-:class:`~pyrseas.dbobject.DbObject` and represents a `PostgreSQL
-foreign data wrapper
+:class:`DbObjectWithOptions` and represents a `PostgreSQL foreign data
+wrapper
<http://www.postgresql.org/docs/current/static/sql-createcreateforeigndatawrapper.html>`_.
For PostgreSQL versions 9.1 and later see also `Foreign Data
<http://www.postgresql.org/docs/current/static/ddl-foreign-data.html>`_
@@ -29,6 +46,8 @@ and `Writing A Foreign Data Wrapper
.. automethod:: ForeignDataWrapper.create
+.. automethod:: ForeignDataWrapper.diff_map
+
Foreign Data Wrapper Dictionary
-------------------------------
@@ -49,9 +68,8 @@ represents the collection of foreign data wrappers in a database.
Foreign Server
--------------
-:class:`ForeignServer` is derived from
-:class:`~pyrseas.dbobject.DbObject` and represents a `PostgreSQL
-foreign server
+:class:`ForeignServer` is derived from :class:`DbObjectWithOptions`
+and represents a `PostgreSQL foreign server
<http://www.postgresql.org/docs/current/static/sql-createserver.html>`_.
.. autoclass:: ForeignServer
@@ -62,6 +80,8 @@ foreign server
.. automethod:: ForeignServer.create
+.. automethod:: ForeignServer.diff_map
+
Foreign Server Dictionary
-------------------------
@@ -82,9 +102,8 @@ that represents the collection of foreign servers in a database.
User Mapping
------------
-:class:`UserMapping` is derived from
-:class:`~pyrseas.dbobject.DbObject` and represents a `PostgreSQL user
-mapping of a user to a foreign server
+:class:`UserMapping` is derived from :class:`DbObjectWithOptions` and
+represents a `PostgreSQL user mapping of a user to a foreign server
<http://www.postgresql.org/docs/current/static/sql-createusermapping.html>`_.
.. autoclass:: UserMapping
@@ -113,8 +132,9 @@ represents the collection of user mappings in a database.
Foreign Table
-------------
-:class:`ForeignTable` is derived from :class:`~pyrseas.table.Table`
-and represents a `PostgreSQL foreign table
+:class:`ForeignTable` is derived from :class:`DbObjectWithOptions` and
+:class:`~pyrseas.dbobject.table.Table`. It represents a `PostgreSQL foreign
+table
<http://www.postgresql.org/docs/current/static/sql-createforeigntable.html>`_
(available on PostgreSQL 9.1 or later).
@@ -126,12 +146,14 @@ and represents a `PostgreSQL foreign table
.. automethod:: ForeignTable.drop
+.. automethod:: ForeignTable.diff_map
+
Foreign Table Dictionary
------------------------
:class:`ForeignTableDict` is derived from
-`~pyrseas.table.ClassDict`. It is a dictionary that represents the
-collection of foreign tables in a database.
+:class:`~pyrseas.dbobject.table.ClassDict`. It is a dictionary that
+represents the collection of foreign tables in a database.
.. autoclass:: ForeignTableDict
View
136 pyrseas/dbobject/foreign.py
@@ -3,30 +3,77 @@
pyrseas.dbobject.foreign
~~~~~~~~~~~~~~~~~~~~~~~~
- This defines eight classes: ForeignDataWrapper, ForeignServer and
- UserMapping derived from DbObject, ForeignDataWrapperDict,
+ This defines nine classes: DbObjectWithOptions derived from
+ DbObject, ForeignDataWrapper, ForeignServer and UserMapping
+ derived from DbObjectWithOptions, ForeignDataWrapperDict,
ForeignServerDict and UserMappingDict derived from DbObjectDict,
- ForeignTable derived from Table and ForeignTableDict derived from
- ClassDict.
+ ForeignTable derived from DbObjectWithOptions and Table, and
+ ForeignTableDict derived from ClassDict.
"""
from pyrseas.dbobject import DbObjectDict, DbObject, quote_id
from pyrseas.dbobject.table import ClassDict, Table
-def options_clause(optdict):
- """Helper function to create the OPTIONS clauses
+class DbObjectWithOptions(DbObject):
+ """Helper class for database objects with OPTIONS clauses"""
- :param optdict: the dictionary of options
- :return: SQL OPTIONS clause
- """
- opts = []
- for opt in optdict:
- (nm, val) = opt.split('=', 1)
- opts.append("%s '%s'" % (nm, val))
- return "OPTIONS (%s)" % ', '.join(opts)
+ def options_clause(self):
+ """Create the OPTIONS clause
+ :param optdict: the dictionary of options
+ :return: SQL OPTIONS clause
+ """
+ opts = []
+ for opt in self.options:
+ (nm, val) = opt.split('=', 1)
+ opts.append("%s '%s'" % (nm, val))
+ return "OPTIONS (%s)" % ', '.join(opts)
+
+ def diff_options(self, newopts):
+ """Compare options lists and generate SQL OPTIONS clause
+
+ :newopts: list of new options
+ :return: SQL OPTIONS clause
+
+ Generate ([ADD|SET|DROP key 'value') clauses from two lists in the
+ form of 'key=value' strings.
+ """
+ def to_dict(optlist):
+ return dict(opt.split('=', 1) for opt in optlist)
-class ForeignDataWrapper(DbObject):
+ oldopts = {}
+ if hasattr(self, 'options'):
+ oldopts = to_dict(self.options)
+ newopts = to_dict(newopts)
+ clauses = []
+ for key, val in newopts.items():
+ if key not in oldopts:
+ clauses.append("%s '%s'" % (key, val))
+ elif val != oldopts[key]:
+ clauses.append("SET %s '%s'" % (key, val))
+ for key, val in oldopts.items():
+ if key not in newopts:
+ clauses.append("DROP %s" % key)
+ return clauses and "OPTIONS (%s)" % ', '.join(clauses) or ''
+
+ def diff_map(self, inobj):
+ """Generate SQL to transform an existing object
+
+ :param inobj: a YAML map defining the new object
+ :return: list of SQL statements
+ """
+ stmts = []
+ newopts = []
+ if hasattr(inobj, 'options'):
+ newopts = inobj.options
+ diff_opts = self.diff_options(newopts)
+ if diff_opts:
+ stmts.append("ALTER %s %s %s" % (
+ self.objtype, self.identifier(), diff_opts))
+ return stmts
+
+
+class ForeignDataWrapper(DbObjectWithOptions):
"""A foreign data wrapper definition"""
objtype = "FOREIGN DATA WRAPPER"
@@ -56,7 +103,7 @@ def create(self):
if hasattr(self, fnc):
clauses.append("%s %s" % (fnc.upper(), getattr(self, fnc)))
if hasattr(self, 'options'):
- clauses.append(options_clause(self.options))
+ clauses.append(self.options_clause())
stmts = ["CREATE FOREIGN DATA WRAPPER %s%s" % (
quote_id(self.name),
clauses and '\n ' + ',\n '.join(clauses) or '')]
@@ -64,6 +111,16 @@ def create(self):
stmts.append(self.comment())
return stmts
+ def diff_map(self, inwrapper):
+ """Generate SQL to transform an existing wrapper
+
+ :param inwrapper: a YAML map defining the new wrapper
+ :return: list of SQL statements
+ """
+ stmts = super(ForeignDataWrapper, self).diff_map(inwrapper)
+ stmts.append(self.diff_description(inwrapper))
+ return stmts
+
QUERY_PRE91 = \
"""SELECT fdwname AS name, CASE WHEN fdwvalidator = 0 THEN NULL
@@ -198,7 +255,7 @@ def _drop(self):
return stmts
-class ForeignServer(DbObject):
+class ForeignServer(DbObjectWithOptions):
"""A foreign server definition"""
objtype = "SERVER"
@@ -237,7 +294,7 @@ def create(self):
if hasattr(self, opt):
clauses.append("%s '%s'" % (opt.upper(), getattr(self, opt)))
if hasattr(self, 'options'):
- options.append(options_clause(self.options))
+ options.append(self.options_clause())
stmts = ["CREATE SERVER %s%s\n FOREIGN DATA WRAPPER %s%s" % (
quote_id(self.name),
clauses and ' ' + ' '.join(clauses) or '',
@@ -247,6 +304,16 @@ def create(self):
stmts.append(self.comment())
return stmts
+ def diff_map(self, inserver):
+ """Generate SQL to transform an existing server
+
+ :param inserver: a YAML map defining the new server
+ :return: list of SQL statements
+ """
+ stmts = super(ForeignServer, self).diff_map(inserver)
+ stmts.append(self.diff_description(inserver))
+ return stmts
+
class ForeignServerDict(DbObjectDict):
"The collection of foreign servers in a database"
@@ -323,17 +390,17 @@ def diff_map(self, inservers):
"""
stmts = []
# check input foreign servers
- for srv in inservers.keys():
- insrv = inservers[srv]
+ for (fdw, srv) in inservers.keys():
+ insrv = inservers[(fdw, srv)]
# does it exist in the database?
- if srv in self:
- stmts.append(self[srv].diff_map(insrv))
+ if (fdw, srv) in self:
+ stmts.append(self[(fdw, srv)].diff_map(insrv))
else:
# check for possible RENAME
if hasattr(insrv, 'oldname'):
oldname = insrv.oldname
try:
- stmts.append(self[oldname].rename(insrv.name))
+ stmts.append(self[(fdw, oldname)].rename(insrv.name))
del self[oldname]
except KeyError as exc:
exc.args = ("Previous name '%s' for dictionary '%s' "
@@ -361,7 +428,7 @@ def _drop(self):
return stmts
-class UserMapping(DbObject):
+class UserMapping(DbObjectWithOptions):
"""A user mapping definition"""
objtype = "USER MAPPING"
@@ -391,7 +458,7 @@ def create(self):
"""
options = []
if hasattr(self, 'options'):
- options.append(options_clause(self.options))
+ options.append(self.options_clause())
stmts = ["CREATE USER MAPPING FOR %s\n SERVER %s%s" % (
self.username == 'PUBLIC' and 'PUBLIC' or
quote_id(self.username), quote_id(self.server),
@@ -466,8 +533,9 @@ def diff_map(self, inusermaps):
if hasattr(inump, 'oldname'):
oldname = inump.oldname
try:
- stmts.append(self[oldname].rename(inump.name))
- del self[oldname]
+ stmts.append(self[(fdw, srv, oldname)].rename(
+ inump.name))
+ del self[(fdw, srv, oldname)]
except KeyError as exc:
exc.args = ("Previous name '%s' for user mapping '%s' "
"not found" % (oldname, inump.name), )
@@ -483,7 +551,7 @@ def diff_map(self, inusermaps):
return stmts
-class ForeignTable(Table):
+class ForeignTable(DbObjectWithOptions, Table):
"""A foreign table definition"""
objtype = "FOREIGN TABLE"
@@ -519,7 +587,7 @@ def create(self):
for col in self.columns:
cols.append(" " + col.add()[0])
if hasattr(self, 'options'):
- options.append(options_clause(self.options))
+ options.append(self.options_clause())
stmts.append("CREATE FOREIGN TABLE %s (\n%s)\n SERVER %s%s" % (
self.qualname(), ",\n".join(cols), self.server,
options and '\n ' + ',\n '.join(options) or ''))
@@ -537,6 +605,16 @@ def drop(self):
"""
return "DROP %s %s" % (self.objtype, self.identifier())
+ def diff_map(self, intable):
+ """Generate SQL to transform an existing table
+
+ :param intable: a YAML map defining the new table
+ :return: list of SQL statements
+ """
+ stmts = super(ForeignTable, self).diff_map(intable)
+ stmts.append(self.diff_description(intable))
+ return stmts
+
class ForeignTableDict(ClassDict):
"The collection of foreign tables in a database"
View
57 tests/dbobject/test_foreign.py
@@ -91,6 +91,18 @@ def test_drop_fd_wrapper(self):
dbsql = self.db.process_map(self.std_map())
self.assertEqual(dbsql[0], "DROP FOREIGN DATA WRAPPER fdw1")
+ def test_alter_wrapper_options(self):
+ "Change foreign data wrapper options"
+ self.db.execute_commit(CREATE_FDW_STMT +
+ " OPTIONS (opt1 'valA', opt2 'valB')")
+ inmap = self.std_map()
+ inmap.update({'foreign data wrapper fdw1': {
+ 'options': ['opt1=valX', 'opt3=valY']}})
+ dbsql = self.db.process_map(inmap)
+ self.assertEqual(fix_indent(dbsql[0]),
+ "ALTER FOREIGN DATA WRAPPER fdw1 OPTIONS "
+ "(SET opt1 'valX', opt3 'valY', DROP opt2)")
+
def test_comment_on_fd_wrapper(self):
"Create a comment for an existing foreign data wrapper"
self.db.execute_commit(CREATE_FDW_STMT)
@@ -196,6 +208,17 @@ def test_drop_server(self):
dbsql = self.db.process_map(inmap)
self.assertEqual(dbsql[0], "DROP SERVER fs1")
+ def test_add_server_options(self):
+ "Add options to a foreign server"
+ self.db.execute(CREATE_FDW_STMT)
+ self.db.execute_commit(CREATE_FS_STMT)
+ inmap = self.std_map()
+ inmap.update({'foreign data wrapper fdw1': {'server fs1': {
+ 'options': ['opt1=valA', 'opt2=valB']}}})
+ dbsql = self.db.process_map(inmap)
+ self.assertEqual(fix_indent(dbsql[0]),
+ "ALTER SERVER fs1 OPTIONS (opt1 'valA', opt2 'valB')")
+
def test_drop_server_wrapper(self):
"Drop an existing foreign data wrapper and its server"
self.db.execute(CREATE_FDW_STMT)
@@ -280,6 +303,20 @@ def test_drop_user_mapping(self):
dbsql = self.db.process_map(self.std_map())
self.assertEqual(dbsql[0], "DROP USER MAPPING FOR PUBLIC SERVER fs1")
+ def test_drop_user_mapping_options(self):
+ "Drop options from a user mapping"
+ self.db.execute(CREATE_FDW_STMT)
+ self.db.execute(CREATE_FS_STMT)
+ self.db.execute_commit(CREATE_UM_STMT +
+ " OPTIONS (opt1 'valA', opt2 'valB')")
+ inmap = self.std_map()
+ inmap.update({'foreign data wrapper fdw1': {'server fs1': {
+ 'user mappings': {'PUBLIC': {}}}}})
+ dbsql = self.db.process_map(inmap)
+ self.assertEqual(fix_indent(dbsql[0]),
+ "ALTER USER MAPPING FOR PUBLIC SERVER fs1 "
+ "OPTIONS (DROP opt1, DROP opt2)")
+
class ForeignTableToMapTestCase(PyrseasTestCase):
"""Test mapping of existing foreign tables"""
@@ -342,7 +379,7 @@ def test_create_foreign_table_options(self):
self.assertEqual(fix_indent(dbsql[0]), CREATE_FT_STMT +
" OPTIONS (user 'jack')")
- def test_bad_map_forein_table(self):
+ def test_bad_map_foreign_table(self):
"Error creating a foreign table with a bad map"
if self.db.version < 90100:
self.skipTest('Only available on PG 9.1')
@@ -378,6 +415,24 @@ def test_drop_foreign_table_server(self):
self.assertEqual(dbsql[0], "DROP FOREIGN TABLE ft1")
self.assertEqual(dbsql[1], "DROP SERVER fs1")
+ def test_alter_foreign_table_options(self):
+ "Alter options for a foreign table"
+ if self.db.version < 90100:
+ self.skipTest('Only available on PG 9.1')
+ self.db.execute(CREATE_FDW_STMT)
+ self.db.execute(CREATE_FS_STMT)
+ self.db.execute_commit(CREATE_FT_STMT +
+ " OPTIONS (opt1 'valA', opt2 'valB')")
+ inmap = self.std_map()
+ inmap.update({'foreign data wrapper fdw1': {'server fs1': {}}})
+ inmap['schema public'].update({'foreign table ft1': {
+ 'columns': [{'c1': {'type': 'integer'}},
+ {'c2': {'type': 'text'}}], 'server': 'fs1',
+ 'options': ['opt1=valX', 'opt2=valY']}})
+ dbsql = self.db.process_map(inmap)
+ self.assertEqual(fix_indent(dbsql[0]), "ALTER FOREIGN TABLE ft1 "
+ "OPTIONS (SET opt1 'valX', SET opt2 'valY')")
+
def suite():
tests = unittest.TestLoader().loadTestsFromTestCase(
Please sign in to comment.
Something went wrong with that request. Please try again.