summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2014-09-17 13:11:49 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2014-09-17 13:11:49 -0400
commit3a6cd72beaeed49b49619e4907cc4b80abedfcb7 (patch)
treec1e79e855f5bc6feacb579bb32a6c6f5906a7f08
parenta985f84ed6223e7a7348dd6126f8de92012b635f (diff)
parentbe57def4b909a447b10fff21bf957c804132b5ec (diff)
downloadsqlalchemy-3a6cd72beaeed49b49619e4907cc4b80abedfcb7.tar.gz
Merge branch 'pr128'
-rw-r--r--doc/build/changelog/changelog_10.rst17
-rw-r--r--doc/build/changelog/migration_10.rst28
-rw-r--r--lib/sqlalchemy/dialects/postgresql/base.py38
-rw-r--r--setup.cfg7
-rw-r--r--test/dialect/postgresql/test_reflection.py117
-rw-r--r--test/requirements.py8
6 files changed, 211 insertions, 4 deletions
diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst
index 9c7f207cc..b00dbb0a7 100644
--- a/doc/build/changelog/changelog_10.rst
+++ b/doc/build/changelog/changelog_10.rst
@@ -22,6 +22,23 @@
on compatibility concerns, see :doc:`/changelog/migration_10`.
.. change::
+ :tags: feature, postgresql
+ :tickets: 2891
+ :pullreq: github:128
+
+ Support has been added for reflection of materialized views
+ and foreign tables, as well as support for materialized views
+ within :meth:`.Inspector.get_view_names`, and a new method
+ :meth:`.PGInspector.get_foreign_table_names` available on the
+ Postgresql version of :class:`.Inspector`. Pull request courtesy
+ Rodrigo Menezes.
+
+ .. seealso::
+
+ :ref:`feature_2891`
+
+
+ .. change::
:tags: feature, orm
Added new event handlers :meth:`.AttributeEvents.init_collection`
diff --git a/doc/build/changelog/migration_10.rst b/doc/build/changelog/migration_10.rst
index 6a48b31fa..d967afa35 100644
--- a/doc/build/changelog/migration_10.rst
+++ b/doc/build/changelog/migration_10.rst
@@ -865,6 +865,34 @@ method that returns information on all available ``ENUM`` types::
:meth:`.PGInspector.get_enums`
+.. _feature_2891:
+
+Postgresql Dialect reflects Materialized Views, Foreign Tables
+--------------------------------------------------------------
+
+Changes are as follows:
+
+* the :class:`Table` construct with ``autoload=True`` will now match a name
+ that exists in the database as a materialized view or foriegn table.
+
+* :meth:`.Inspector.get_view_names` will return plain and materialized view
+ names.
+
+* :meth:`.Inspector.get_table_names` does **not** change for Postgresql, it
+ continues to return only the names of plain tables.
+
+* A new method :meth:`.PGInspector.get_foreign_table_names` is added which
+ will return the names of tables that are specifically marked as "foreign"
+ in the Postgresql schema tables.
+
+The change to reflection involves adding ``'m'`` and ``'f'`` to the list
+of qualifiers we use when querying ``pg_class.relkind``, but this change
+is new in 1.0.0 to avoid any backwards-incompatible surprises for those
+running 0.9 in production.
+
+:ticket:`2891`
+
+
MySQL internal "no such table" exceptions not passed to event handlers
----------------------------------------------------------------------
diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py
index 575d2a6dd..b9a0d461b 100644
--- a/lib/sqlalchemy/dialects/postgresql/base.py
+++ b/lib/sqlalchemy/dialects/postgresql/base.py
@@ -401,6 +401,7 @@ The value passed to the keyword argument will be simply passed through to the
underlying CREATE INDEX command, so it *must* be a valid index type for your
version of PostgreSQL.
+
Special Reflection Options
--------------------------
@@ -1679,6 +1680,19 @@ class PGInspector(reflection.Inspector):
schema = schema or self.default_schema_name
return self.dialect._load_enums(self.bind, schema)
+ def get_foreign_table_names(self, schema=None):
+ """Return a list of FOREIGN TABLE names.
+
+ Behavior is similar to that of :meth:`.Inspector.get_table_names`,
+ except that the list is limited to those tables tha report a
+ ``relkind`` value of ``f``.
+
+ .. versionadded:: 1.0.0
+
+ """
+ schema = schema or self.default_schema_name
+ return self.dialect._get_foreign_table_names(self.bind, schema)
+
class CreateEnumType(schema._CreateDropBase):
__visit_name__ = "create_enum_type"
@@ -2024,7 +2038,7 @@ class PGDialect(default.DefaultDialect):
FROM pg_catalog.pg_class c
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE (%s)
- AND c.relname = :table_name AND c.relkind in ('r','v')
+ AND c.relname = :table_name AND c.relkind in ('r', 'v', 'm', 'f')
""" % schema_where_clause
# Since we're binding to unicode, table_name and schema_name must be
# unicode.
@@ -2078,6 +2092,24 @@ class PGDialect(default.DefaultDialect):
return [row[0] for row in result]
@reflection.cache
+ def _get_foreign_table_names(self, connection, schema=None, **kw):
+ if schema is not None:
+ current_schema = schema
+ else:
+ current_schema = self.default_schema_name
+
+ result = connection.execute(
+ sql.text("SELECT relname FROM pg_class c "
+ "WHERE relkind = 'f' "
+ "AND '%s' = (select nspname from pg_namespace n "
+ "where n.oid = c.relnamespace) " %
+ current_schema,
+ typemap={'relname': sqltypes.Unicode}
+ )
+ )
+ return [row[0] for row in result]
+
+ @reflection.cache
def get_view_names(self, connection, schema=None, **kw):
if schema is not None:
current_schema = schema
@@ -2086,7 +2118,7 @@ class PGDialect(default.DefaultDialect):
s = """
SELECT relname
FROM pg_class c
- WHERE relkind = 'v'
+ WHERE relkind IN ('m', 'v')
AND '%(schema)s' = (select nspname from pg_namespace n
where n.oid = c.relnamespace)
""" % dict(schema=current_schema)
@@ -2448,7 +2480,7 @@ class PGDialect(default.DefaultDialect):
pg_attribute a
on t.oid=a.attrelid and %s
WHERE
- t.relkind = 'r'
+ t.relkind IN ('r', 'v', 'f', 'm')
and t.oid = :table_oid
and ix.indisprimary = 'f'
ORDER BY
diff --git a/setup.cfg b/setup.cfg
index 698c4b037..b70086605 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -26,6 +26,13 @@ profile_file=test/profiles.txt
# create database link test_link connect to scott identified by tiger using 'xe';
oracle_db_link = test_link
+# host name of a postgres database that has the postgres_fdw extension.
+# to create this run:
+# CREATE EXTENSION postgres_fdw;
+# GRANT USAGE ON FOREIGN DATA WRAPPER postgres_fdw TO public;
+# this can be localhost to create a loopback foreign table
+postgres_test_db_link = localhost
+
[db]
default=sqlite:///:memory:
diff --git a/test/dialect/postgresql/test_reflection.py b/test/dialect/postgresql/test_reflection.py
index bab41b0f7..b8b9be3de 100644
--- a/test/dialect/postgresql/test_reflection.py
+++ b/test/dialect/postgresql/test_reflection.py
@@ -13,8 +13,123 @@ import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import base as postgresql
-class DomainReflectionTest(fixtures.TestBase, AssertsExecutionResults):
+class ForeignTableReflectionTest(fixtures.TablesTest, AssertsExecutionResults):
+ """Test reflection on foreign tables"""
+
+ __requires__ = 'postgresql_test_dblink',
+ __only_on__ = 'postgresql >= 9.3'
+ __backend__ = True
+
+ @classmethod
+ def define_tables(cls, metadata):
+ from sqlalchemy.testing import config
+ dblink = config.file_config.get(
+ 'sqla_testing', 'postgres_test_db_link')
+
+ testtable = Table(
+ 'testtable', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('data', String(30)))
+
+ for ddl in [
+ "CREATE SERVER test_server FOREIGN DATA WRAPPER postgres_fdw "
+ "OPTIONS (dbname 'test', host '%s')" % dblink,
+ "CREATE USER MAPPING FOR public \
+ SERVER test_server options (user 'scott', password 'tiger')",
+ "CREATE FOREIGN TABLE test_foreigntable ( "
+ " id INT, "
+ " data VARCHAR(30) "
+ ") SERVER test_server OPTIONS (table_name 'testtable')",
+ ]:
+ sa.event.listen(metadata, "after_create", sa.DDL(ddl))
+
+ for ddl in [
+ 'DROP FOREIGN TABLE test_foreigntable',
+ 'DROP USER MAPPING FOR public SERVER test_server',
+ "DROP SERVER test_server"
+ ]:
+ sa.event.listen(metadata, "before_drop", sa.DDL(ddl))
+
+ def test_foreign_table_is_reflected(self):
+ metadata = MetaData(testing.db)
+ table = Table('test_foreigntable', metadata, autoload=True)
+ eq_(set(table.columns.keys()), set(['id', 'data']),
+ "Columns of reflected foreign table didn't equal expected columns")
+ def test_get_foreign_table_names(self):
+ inspector = inspect(testing.db)
+ with testing.db.connect() as conn:
+ ft_names = inspector.get_foreign_table_names()
+ eq_(ft_names, ['test_foreigntable'])
+
+ def test_get_table_names_no_foreign(self):
+ inspector = inspect(testing.db)
+ with testing.db.connect() as conn:
+ names = inspector.get_table_names()
+ eq_(names, ['testtable'])
+
+
+class MaterialiedViewReflectionTest(
+ fixtures.TablesTest, AssertsExecutionResults):
+ """Test reflection on materialized views"""
+
+ __only_on__ = 'postgresql >= 9.3'
+ __backend__ = True
+
+ @classmethod
+ def define_tables(cls, metadata):
+ testtable = Table(
+ 'testtable', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('data', String(30)))
+
+ # insert data before we create the view
+ @sa.event.listens_for(testtable, "after_create")
+ def insert_data(target, connection, **kw):
+ connection.execute(
+ target.insert(),
+ {"id": 89, "data": 'd1'}
+ )
+
+ materialized_view = sa.DDL(
+ "CREATE MATERIALIZED VIEW test_mview AS "
+ "SELECT * FROM testtable")
+
+ plain_view = sa.DDL(
+ "CREATE VIEW test_regview AS "
+ "SELECT * FROM testtable")
+
+ sa.event.listen(testtable, 'after_create', plain_view)
+ sa.event.listen(testtable, 'after_create', materialized_view)
+ sa.event.listen(
+ testtable, 'before_drop',
+ sa.DDL("DROP MATERIALIZED VIEW test_mview")
+ )
+ sa.event.listen(
+ testtable, 'before_drop',
+ sa.DDL("DROP VIEW test_regview")
+ )
+
+ def test_mview_is_reflected(self):
+ metadata = MetaData(testing.db)
+ table = Table('test_mview', metadata, autoload=True)
+ eq_(set(table.columns.keys()), set(['id', 'data']),
+ "Columns of reflected mview didn't equal expected columns")
+
+ def test_mview_select(self):
+ metadata = MetaData(testing.db)
+ table = Table('test_mview', metadata, autoload=True)
+ eq_(
+ table.select().execute().fetchall(),
+ [(89, 'd1',)]
+ )
+
+ def test_get_view_names(self):
+ insp = inspect(testing.db)
+ eq_(set(insp.get_view_names()), set(['test_mview', 'test_regview']))
+
+
+class DomainReflectionTest(fixtures.TestBase, AssertsExecutionResults):
"""Test PostgreSQL domains"""
__only_on__ = 'postgresql > 8.3'
diff --git a/test/requirements.py b/test/requirements.py
index 7eeabef2b..14bb25691 100644
--- a/test/requirements.py
+++ b/test/requirements.py
@@ -706,6 +706,14 @@ class DefaultRequirements(SuiteRequirements):
)
@property
+ def postgresql_test_dblink(self):
+ return skip_if(
+ lambda config: not config.file_config.has_option(
+ 'sqla_testing', 'postgres_test_db_link'),
+ "postgres_test_db_link option not specified in config"
+ )
+
+ @property
def percent_schema_names(self):
return skip_if(
[