Index: django/conf/global_settings.py
===================================================================
--- django/conf/global_settings.py	(revision 17914)
+++ django/conf/global_settings.py	(working copy)
@@ -407,6 +407,9 @@
 DEFAULT_TABLESPACE = ''
 DEFAULT_INDEX_TABLESPACE = ''
 
+# The default database schema to use for tables
+DEFAULT_SCHEMA = None
+
 # Default X-Frame-Options header value
 X_FRAME_OPTIONS = 'SAMEORIGIN'
 
Index: django/db/models/fields/related.py
===================================================================
--- django/db/models/fields/related.py	(revision 17914)
+++ django/db/models/fields/related.py	(working copy)
@@ -1,6 +1,6 @@
 from operator import attrgetter
 
-from django.db import connection, router
+from django.db import connection, router, QName
 from django.db.backends import util
 from django.db.models import signals, get_model
 from django.db.models.fields import (AutoField, Field, IntegerField,
@@ -568,11 +568,11 @@
             # dealing with PK values.
             fk = self.through._meta.get_field(self.source_field_name)
             source_col = fk.column
-            join_table = self.through._meta.db_table
             connection = connections[db]
             qn = connection.ops.quote_name
+            join_alias = connection.qname(self.through)
             qs = qs.extra(select={'_prefetch_related_val':
-                                      '%s.%s' % (qn(join_table), qn(source_col))})
+                                      '%s.%s' % (join_alias, qn(source_col))})
             select_attname = fk.rel.get_related_field().get_attname()
             return (qs,
                     attrgetter('_prefetch_related_val'),
@@ -1085,6 +1085,7 @@
         to = to.lower()
     meta = type('Meta', (object,), {
         'db_table': field._get_m2m_db_table(klass._meta),
+        'db_schema': field._get_m2m_db_schema(klass._meta),
         'managed': managed,
         'auto_created': klass,
         'app_label': klass._meta.app_label,
@@ -1121,8 +1122,12 @@
             through=kwargs.pop('through', None))
 
         self.db_table = kwargs.pop('db_table', None)
+        self.db_schema = kwargs.pop('db_schema', None)
         if kwargs['rel'].through is not None:
             assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
+            assert self.db_schema is None, "Cannot specify a db_schema if an intermediary model is used."
+        # TODO: by default use the containing model's db_schema
+            
 
         Field.__init__(self, **kwargs)
 
@@ -1142,6 +1147,22 @@
             return util.truncate_name('%s_%s' % (opts.db_table, self.name),
                                       connection.ops.max_name_length())
 
+    def _get_m2m_db_schema(self, opts):
+        "Function that can be curried to provide the m2m schema name for this relation"
+        if self.rel.through is not None and self.rel.through._meta.db_schema:
+            return self.rel.through._meta.db_schema
+        elif self.db_schema:
+            return self.db_schema
+        else:
+            return opts.db_schema
+
+
+    def _get_m2m_qualified_name(self, opts):
+        "Function that can be curried to provide the qualified m2m table name for this relation"
+        schema = self._get_m2m_db_schema(opts)
+        table = self._get_m2m_db_table(opts)
+        return QName(schema, table, False)
+
     def _get_m2m_attr(self, related, attr):
         "Function that can be curried to provide the source accessor or DB column name for the m2m table"
         cache_attr = '_m2m_%s_cache' % attr
@@ -1212,6 +1233,9 @@
 
         # Set up the accessor for the m2m table name for the relation
         self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
+        self.m2m_db_schema = curry(self._get_m2m_db_schema, cls._meta)
+        self.m2m_qualified_name = curry(self._get_m2m_qualified_name,
+                                        cls._meta)
 
         # Populate some necessary rel arguments so that cross-app relations
         # work correctly.
@@ -1223,7 +1247,7 @@
         if isinstance(self.rel.to, basestring):
             target = self.rel.to
         else:
-            target = self.rel.to._meta.db_table
+            target = self.rel.to._meta.qualified_name
         cls._meta.duplicate_targets[self.column] = (target, "m2m")
 
     def contribute_to_related_class(self, cls, related):
Index: django/db/models/query.py
===================================================================
--- django/db/models/query.py	(revision 17914)
+++ django/db/models/query.py	(working copy)
@@ -1558,7 +1558,7 @@
         A dict mapping column names to model field names.
         """
         if not hasattr(self, '_model_fields'):
-            converter = connections[self.db].introspection.table_name_converter
+            converter = connections[self.db].introspection.identifier_converter
             self._model_fields = {}
             for field in self.model._meta.fields:
                 name, column = field.get_attname_column()
Index: django/db/models/sql/query.py
===================================================================
--- django/db/models/sql/query.py	(revision 17914)
+++ django/db/models/sql/query.py	(working copy)
@@ -16,7 +16,6 @@
 from django.db.models import signals
 from django.db.models.expressions import ExpressionNode
 from django.db.models.fields import FieldDoesNotExist
-from django.db.models.query_utils import InvalidQuery
 from django.db.models.sql import aggregates as base_aggregates_module
 from django.db.models.sql.constants import *
 from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin
@@ -59,7 +58,7 @@
     def get_columns(self):
         if self.cursor is None:
             self._execute_query()
-        converter = connections[self.using].introspection.table_name_converter
+        converter = connections[self.using].introspection.identifier_converter
         return [converter(column_meta[0])
                 for column_meta in self.cursor.description]
 
@@ -638,7 +637,7 @@
         Callback used by deferred_to_columns(). The "target" parameter should
         be a set instance.
         """
-        table = model._meta.db_table
+        table = model._meta.qualified_name
         if table not in target:
             target[table] = set()
         for field in fields:
@@ -659,10 +658,13 @@
             self.alias_refcount[alias] += 1
             return alias, False
 
-        # Create a new alias for this table.
-        if current:
+        # Create a new alias for this table if this table is already used
+        # in the query, or if there is the same table name used in the query
+        # under different schema qualified name.
+        if current or table_name[1] in [t[1] for t in self.tables]:
             alias = '%s%d' % (self.alias_prefix, len(self.alias_map) + 1)
-            current.append(alias)
+            if current:
+                current.append(alias)
         else:
             # The first occurence of a table uses the table name directly.
             alias = table_name
@@ -832,7 +834,7 @@
             alias = self.tables[0]
             self.ref_alias(alias)
         else:
-            alias = self.join((None, self.model._meta.db_table, None, None))
+            alias = self.join((None, self.model._meta.qualified_name, None, None))
         return alias
 
     def count_active_tables(self):
@@ -945,7 +947,7 @@
                     seen[model] = root_alias
                 else:
                     link_field = opts.get_ancestor_link(model)
-                    seen[model] = self.join((root_alias, model._meta.db_table,
+                    seen[model] = self.join((root_alias, model._meta.qualified_name,
                             link_field.column, model._meta.pk.column))
         self.included_inherited_models = seen
 
@@ -1329,7 +1331,7 @@
                                     (id(opts), lhs_col), ()))
                             dupe_set.add((opts, lhs_col))
                         opts = int_model._meta
-                        alias = self.join((alias, opts.db_table, lhs_col,
+                        alias = self.join((alias, opts.qualified_name, lhs_col,
                                 opts.pk.column), exclusions=exclusions)
                         joins.append(alias)
                         exclusions.add(alias)
@@ -1355,12 +1357,12 @@
                         (table1, from_col1, to_col1, table2, from_col2,
                                 to_col2, opts, target) = cached_data
                     else:
-                        table1 = field.m2m_db_table()
+                        table1 = field.m2m_qualified_name()
                         from_col1 = opts.get_field_by_name(
                             field.m2m_target_field_name())[0].column
                         to_col1 = field.m2m_column_name()
                         opts = field.rel.to._meta
-                        table2 = opts.db_table
+                        table2 = opts.qualified_name
                         from_col2 = field.m2m_reverse_name()
                         to_col2 = opts.get_field_by_name(
                             field.m2m_reverse_target_field_name())[0].column
@@ -1388,7 +1390,7 @@
                     else:
                         opts = field.rel.to._meta
                         target = field.rel.get_related_field()
-                        table = opts.db_table
+                        table = opts.qualified_name
                         from_col = field.column
                         to_col = target.column
                         orig_opts._join_cache[name] = (table, from_col, to_col,
@@ -1410,12 +1412,12 @@
                         (table1, from_col1, to_col1, table2, from_col2,
                                 to_col2, opts, target) = cached_data
                     else:
-                        table1 = field.m2m_db_table()
+                        table1 = field.m2m_qualified_name()
                         from_col1 = opts.get_field_by_name(
                             field.m2m_reverse_target_field_name())[0].column
                         to_col1 = field.m2m_reverse_name()
                         opts = orig_field.opts
-                        table2 = opts.db_table
+                        table2 = opts.qualified_name
                         from_col2 = field.m2m_column_name()
                         to_col2 = opts.get_field_by_name(
                             field.m2m_target_field_name())[0].column
@@ -1439,7 +1441,7 @@
                         local_field = opts.get_field_by_name(
                                 field.rel.field_name)[0]
                         opts = orig_field.opts
-                        table = opts.db_table
+                        table = opts.qualified_name
                         from_col = local_field.column
                         to_col = field.column
                         # In case of a recursive FK, use the to_field for
@@ -1722,7 +1724,7 @@
         else:
             opts = self.model._meta
             if not self.select:
-                count = self.aggregates_module.Count((self.join((None, opts.db_table, None, None)), opts.pk.column),
+                count = self.aggregates_module.Count((self.join((None, opts.qualified_name, None, None)), opts.pk.column),
                                          is_summary=True, distinct=True)
             else:
                 # Because of SQL portability issues, multi-column, distinct
Index: django/db/models/sql/subqueries.py
===================================================================
--- django/db/models/sql/subqueries.py	(revision 17914)
+++ django/db/models/sql/subqueries.py	(working copy)
@@ -41,7 +41,7 @@
             where = self.where_class()
             where.add((Constraint(None, field.column, field), 'in',
                     pk_list[offset:offset + GET_ITERATOR_CHUNK_SIZE]), AND)
-            self.do_query(self.model._meta.db_table, where, using=using)
+            self.do_query(self.model._meta.qualified_name, where, using=using)
 
 class UpdateQuery(Query):
     """
Index: django/db/models/sql/compiler.py
===================================================================
--- django/db/models/sql/compiler.py	(revision 17914)
+++ django/db/models/sql/compiler.py	(working copy)
@@ -1,7 +1,8 @@
 from itertools import izip
 
+from django.conf import settings
 from django.core.exceptions import FieldError
-from django.db import transaction
+from django.db import transaction, QName
 from django.db.backends.util import truncate_name
 from django.db.models.query_utils import select_related_descend
 from django.db.models.sql.constants import *
@@ -27,7 +28,7 @@
         # cleaned. We are not using a clone() of the query here.
         """
         if not self.query.tables:
-            self.query.join((None, self.query.model._meta.db_table, None, None))
+            self.query.join((None, self.query.model._meta.qualified_name, None, None))
         if (not self.query.select and self.query.default_cols and not
                 self.query.included_inherited_models):
             self.query.setup_inherited_models()
@@ -42,11 +43,13 @@
         """
         if name in self.quote_cache:
             return self.quote_cache[name]
-        if ((name in self.query.alias_map and name not in self.query.table_map) or
-                name in self.query.extra_select):
+        if name in self.query.alias_map and not isinstance(name, tuple):
             self.quote_cache[name] = name
             return name
-        r = self.connection.ops.quote_name(name)
+        if isinstance(name, tuple):
+            r = self.connection.ops.qualified_name(name)
+        else:
+            r = self.connection.ops.quote_name(name)
         self.quote_cache[name] = r
         return r
 
@@ -281,8 +284,10 @@
                         alias = start_alias
                     else:
                         link_field = opts.get_ancestor_link(model)
-                        alias = self.query.join((start_alias, model._meta.db_table,
-                                link_field.column, model._meta.pk.column))
+                        alias = self.query.join((start_alias,
+                                                 model._meta.qualified_name,
+                                                 link_field.column,
+                                                 model._meta.pk.column))
                     seen[model] = alias
             else:
                 # If we're starting from the base model of the queryset, the
@@ -508,15 +513,12 @@
         qn2 = self.connection.ops.quote_name
         first = True
         for alias in self.query.tables:
-            if not self.query.alias_refcount[alias]:
+            # Extra tables can end up in self.tables, but not in the
+            # alias_map if they aren't in a join. That's OK. We skip them.
+            if not self.query.alias_refcount[alias] or alias not in self.query.alias_map:
                 continue
-            try:
-                name, alias, join_type, lhs, lhs_col, col, nullable = self.query.alias_map[alias]
-            except KeyError:
-                # Extra tables can end up in self.tables, but not in the
-                # alias_map if they aren't in a join. That's OK. We skip them.
-                continue
-            alias_str = (alias != name and ' %s' % alias or '')
+            name, alias, join_type, lhs, lhs_col, col, nullable = self.query.alias_map[alias]
+            alias_str = alias != name and ' %s' % qn(alias) or ''
             if join_type and not first:
                 result.append('%s %s%s ON (%s.%s = %s.%s)'
                         % (join_type, qn(name), alias_str, qn(lhs),
@@ -526,13 +528,17 @@
                 result.append('%s%s%s' % (connector, qn(name), alias_str))
             first = False
         for t in self.query.extra_tables:
-            alias, unused = self.query.table_alias(t)
-            # Only add the alias if it's not already present (the table_alias()
-            # calls increments the refcount, so an alias refcount of one means
-            # this is the only reference.
-            if alias not in self.query.alias_map or self.query.alias_refcount[alias] == 1:
+            # Plain string table names are assumed to be in default schema
+            if not isinstance(t, QName):
+                t = QName(self.connection.schema, t, False)
+            # Only add the table if it is not already in the query.
+            if t not in self.query.table_map or self.query.alias_refcount[t] == 0:
+                # This will add the table into the query properly, however we
+                # are not interested in the alias it gets, we add it as a
+                # plain table.
+                self.query.table_alias(t)
                 connector = not first and ', ' or ''
-                result.append('%s%s' % (connector, qn(alias)))
+                result.append('%s%s' % (connector, qn(t)))
                 first = False
         return result, []
 
@@ -546,7 +552,7 @@
             if (len(self.query.model._meta.fields) == len(self.query.select) and
                 self.connection.features.allows_group_by_pk):
                 self.query.group_by = [
-                    (self.query.model._meta.db_table, self.query.model._meta.pk.column)
+                    (self.query.model._meta.qualified_name, self.query.model._meta.pk.column)
                 ]
 
             group_by = self.query.group_by or []
@@ -614,7 +620,7 @@
             # what "used" specifies).
             avoid = avoid_set.copy()
             dupe_set = orig_dupe_set.copy()
-            table = f.rel.to._meta.db_table
+            table = f.rel.to._meta.qualified_name
             promote = nullable or f.null
             if model:
                 int_opts = opts
@@ -635,7 +641,7 @@
                                 ()))
                         dupe_set.add((opts, lhs_col))
                     int_opts = int_model._meta
-                    alias = self.query.join((alias, int_opts.db_table, lhs_col,
+                    alias = self.query.join((alias, int_opts.qualified_name, lhs_col,
                             int_opts.pk.column), exclusions=used,
                             promote=promote)
                     alias_chain.append(alias)
@@ -687,7 +693,7 @@
                 # what "used" specifies).
                 avoid = avoid_set.copy()
                 dupe_set = orig_dupe_set.copy()
-                table = model._meta.db_table
+                table = model._meta.qualified_name
 
                 int_opts = opts
                 alias = root_alias
@@ -710,7 +716,7 @@
                             dupe_set.add((opts, lhs_col))
                         int_opts = int_model._meta
                         alias = self.query.join(
-                            (alias, int_opts.db_table, lhs_col, int_opts.pk.column),
+                            (alias, int_opts.qualified_name, lhs_col, int_opts.pk.column),
                             exclusions=used, promote=True, reuse=used
                         )
                         alias_chain.append(alias)
@@ -775,7 +781,7 @@
                         # into `resolve_columns` because it wasn't selected.
                         only_load = self.deferred_to_columns()
                         if only_load:
-                            db_table = self.query.model._meta.db_table
+                            db_table = self.query.model._meta.qualified_name
                             fields = [f for f in fields if db_table in only_load and
                                       f.column in only_load[db_table]]
                     row = self.resolve_columns(row, fields)
@@ -856,8 +862,9 @@
         # We don't need quote_name_unless_alias() here, since these are all
         # going to be column names (so we can avoid the extra overhead).
         qn = self.connection.ops.quote_name
+        qn3 = self.connection.ops.qualified_name
         opts = self.query.model._meta
-        result = ['INSERT INTO %s' % qn(opts.db_table)]
+        result = ['INSERT INTO %s' % qn3(opts.qualified_name)]
 
         has_fields = bool(self.query.fields)
         fields = self.query.fields if has_fields else [opts.pk]
@@ -887,7 +894,7 @@
             ]
         if self.return_id and self.connection.features.can_return_id_from_insert:
             params = params[0]
-            col = "%s.%s" % (qn(opts.db_table), qn(opts.pk.column))
+            col = "%s.%s" % (qn3(opts.qualified_name), qn(opts.pk.column))
             result.append("VALUES (%s)" % ", ".join(placeholders[0]))
             r_fmt, r_params = self.connection.ops.return_insert_id()
             result.append(r_fmt % col)
@@ -913,7 +920,7 @@
         if self.connection.features.can_return_id_from_insert:
             return self.connection.ops.fetch_returned_insert_id(cursor)
         return self.connection.ops.last_insert_id(cursor,
-                self.query.model._meta.db_table, self.query.model._meta.pk.column)
+                self.query.model._meta.qualified_name, self.query.model._meta.pk.column)
 
 
 class SQLDeleteCompiler(SQLCompiler):
@@ -939,9 +946,15 @@
         self.pre_sql_setup()
         if not self.query.values:
             return '', ()
-        table = self.query.tables[0]
+        opts = self.query.model._meta
+        table = self.connection.ops.qualified_name(opts.qualified_name)
         qn = self.quote_name_unless_alias
-        result = ['UPDATE %s' % qn(table)]
+        alias = qn(self.query.tables[0])
+        if table == alias:
+            alias = ''
+        else:
+            alias = ' %s' % alias
+        result = ['UPDATE %s%s' % (table, alias)]
         result.append('SET')
         values, update_params = [], []
         for field, model, val in self.query.values:
Index: django/db/models/options.py
===================================================================
--- django/db/models/options.py	(revision 17914)
+++ django/db/models/options.py	(working copy)
@@ -2,6 +2,7 @@
 from bisect import bisect
 
 from django.conf import settings
+from django.db import QName
 from django.db.models.related import RelatedObject
 from django.db.models.fields.related import ManyToManyRel
 from django.db.models.fields import AutoField, FieldDoesNotExist
@@ -17,7 +18,7 @@
 DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
                  'unique_together', 'permissions', 'get_latest_by',
                  'order_with_respect_to', 'app_label', 'db_tablespace',
-                 'abstract', 'managed', 'proxy', 'auto_created')
+                 'abstract', 'managed', 'proxy', 'auto_created', 'db_schema')
 
 class Options(object):
     def __init__(self, meta, app_label=None):
@@ -26,6 +27,8 @@
         self.module_name, self.verbose_name = None, None
         self.verbose_name_plural = None
         self.db_table = ''
+        self.db_schema = ''
+        self.qualified_name = QName(schema=None, table='', db_format=False)
         self.ordering = []
         self.unique_together =  []
         self.permissions =  []
@@ -112,7 +115,12 @@
         # If the db_table wasn't provided, use the app_label + module_name.
         if not self.db_table:
             self.db_table = "%s_%s" % (self.app_label, self.module_name)
+            # TODO: Using connection.ops is wrong: in multidb setup this doesn't work
+            # correctly except if different connections happen to have the same
+            # max_name_length.
             self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
+        self.qualified_name = QName(schema=self.db_schema, table=self.db_table,
+                                    db_format=False)
 
     def _prepare(self, model):
         if self.order_with_respect_to:
@@ -193,6 +201,8 @@
         self.pk = target._meta.pk
         self.proxy_for_model = target
         self.db_table = target._meta.db_table
+        self.db_schema = target._meta.db_schema
+        self.qualified_name = target._meta.qualified_name
 
     def __repr__(self):
         return '<Options for %s>' % self.object_name
Index: django/db/__init__.py
===================================================================
--- django/db/__init__.py	(revision 17914)
+++ django/db/__init__.py	(working copy)
@@ -3,11 +3,17 @@
 from django.core.exceptions import ImproperlyConfigured
 from django.db.utils import (ConnectionHandler, ConnectionRouter,
     load_backend, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError)
+from collections import namedtuple
 
+# All table names must be QNames. The db_format argument
+# tells if the qualified name is in a format that is ready
+# to be used in the database or if the qname needs to be
+# converted first.
+QName = namedtuple("QName", "schema table db_format")
+
 __all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError',
     'IntegrityError', 'DEFAULT_DB_ALIAS')
 
-
 if DEFAULT_DB_ALIAS not in settings.DATABASES:
     raise ImproperlyConfigured("You must define a '%s' database" % DEFAULT_DB_ALIAS)
 
Index: django/db/utils.py
===================================================================
--- django/db/utils.py	(revision 17914)
+++ django/db/utils.py	(working copy)
@@ -78,10 +78,17 @@
             conn['ENGINE'] = 'django.db.backends.dummy'
         conn.setdefault('OPTIONS', {})
         conn.setdefault('TIME_ZONE', 'UTC' if settings.USE_TZ else settings.TIME_ZONE)
-        for setting in ['NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']:
+        conn.setdefault('SCHEMA', settings.DEFAULT_SCHEMA)
+        for setting in ['NAME', 'USER', 'PASSWORD', 'HOST', 'PORT', 'SCHEMA']:
             conn.setdefault(setting, '')
         for setting in ['TEST_CHARSET', 'TEST_COLLATION', 'TEST_NAME', 'TEST_MIRROR']:
             conn.setdefault(setting, None)
+        # Some databases need a unique prefix for schemas to avoid name
+        # collisions between production database or another testing database
+        # in the same instance. This needs to be a string unlike the other
+        # TEST_ settings above.
+        conn.setdefault('TEST_SCHEMA_PREFIX', '')
+        conn.setdefault('TEST_SCHEMAS', [])
 
     def __getitem__(self, alias):
         if hasattr(self._connections, alias):
Index: django/db/backends/postgresql_psycopg2/introspection.py
===================================================================
--- django/db/backends/postgresql_psycopg2/introspection.py	(revision 17914)
+++ django/db/backends/postgresql_psycopg2/introspection.py	(working copy)
@@ -1,3 +1,4 @@
+from django.db import QName
 from django.db.backends import BaseDatabaseIntrospection
 
 
@@ -21,50 +22,111 @@
         1266: 'TimeField',
         1700: 'DecimalField',
     }
+
+    def get_schema_list(self, cursor):
+        cursor.execute("""
+            SELECT n.nspname
+            FROM pg_catalog.pg_namespace n
+            WHERE n.nspname != 'information_schema' AND n.nspname not like 'pg_%%'""")
+        return [row[0] for row in cursor.fetchall()]
         
-    def get_table_list(self, cursor):
-        "Returns a list of table names in the current database."
+    def get_visible_tables_list(self, cursor):
+        """
+        Returns a list of all by-default visible table names in the current
+        database.
+        """
+        sql = """
+            SELECT n.nspname, c.relname
+            FROM pg_catalog.pg_class c
+            LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
+            WHERE c.relkind IN ('r', 'v', '')
+                AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
+                AND (pg_catalog.pg_table_is_visible(c.oid)"""
+        # We must add the default schema to always visible schemas to make
+        # things work nicely.
+        if self.connection.schema:
+            sql += " OR n.nspname = %s)"
+            cursor.execute(sql, (self.connection.schema,))
+        else:
+            cursor.execute(sql + ')')
+        return [QName(row[0], row[1], True) for row in cursor.fetchall()]
+    
+    def get_qualified_tables_list(self, cursor, schemas):
+        """
+        Returns schema qualified names of all tables in the current database.
+        """
+        if not schemas:
+            return []
         cursor.execute("""
-            SELECT c.relname
+            SELECT n.nspname, c.relname
             FROM pg_catalog.pg_class c
             LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
             WHERE c.relkind IN ('r', 'v', '')
-                AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
-                AND pg_catalog.pg_table_is_visible(c.oid)""")
-        return [row[0] for row in cursor.fetchall()]
+                AND n.nspname IN %s""", (tuple(schemas),))
+        return [QName(row[0], row[1], True) for row in cursor.fetchall()]
 
-    def get_table_description(self, cursor, table_name):
+    def get_table_description(self, cursor, qname):
         "Returns a description of the table, with the DB-API cursor.description interface."
         # As cursor.description does not return reliably the nullable property,
         # we have to query the information_schema (#7783)
-        cursor.execute("""
-            SELECT column_name, is_nullable
-            FROM information_schema.columns
-            WHERE table_name = %s""", [table_name])
+        qname = self.qname_converter(qname)
+        if not qname.schema:
+            cursor.execute("""
+                SELECT column_name, is_nullable
+                FROM information_schema.columns
+                WHERE table_name = %s""",
+                [qname.table])
+        else:
+            cursor.execute("""
+                SELECT column_name, is_nullable
+                FROM information_schema.columns
+                WHERE table_schema = %s and table_name = %s""",
+                [qname.schema, qname.table])
         null_map = dict(cursor.fetchall())
-        cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name))
+        cursor.execute(
+            "SELECT * FROM %s LIMIT 1" % self.connection.ops.qualified_name(qname))
         return [tuple([item for item in line[:6]] + [null_map[line[0]]==u'YES'])
-            for line in cursor.description]
+                for line in cursor.description]
 
-    def get_relations(self, cursor, table_name):
+    def get_relations(self, cursor, qname):
         """
         Returns a dictionary of {field_index: (field_index_other_table, other_table)}
-        representing all relationships to the given table. Indexes are 0-based.
+        representing all relationships to the given table. Indexes are 0-based. The
+        other_table will be in qualified format.
         """
-        cursor.execute("""
-            SELECT con.conkey, con.confkey, c2.relname
-            FROM pg_constraint con, pg_class c1, pg_class c2
-            WHERE c1.oid = con.conrelid
-                AND c2.oid = con.confrelid
-                AND c1.relname = %s
-                AND con.contype = 'f'""", [table_name])
+        qname = self.qname_converter(qname)
+        if not qname.schema:
+            cursor.execute("""
+                SELECT con.conkey, con.confkey, nsp2.nspname, c2.relname
+                FROM pg_constraint con, pg_class c1, pg_class c2,
+                     pg_namespace nsp2
+                WHERE c1.oid = con.conrelid
+                    AND c2.oid = con.confrelid
+                    AND nsp2.oid = c2.relnamespace
+                    AND c1.relname = %s
+                    AND con.contype = 'f'""",
+                [qname.table])
+        else:
+            cursor.execute("""
+                SELECT con.conkey, con.confkey, nsp2.nspname, c2.relname
+                FROM pg_constraint con, pg_class c1, pg_class c2,
+                     pg_namespace nsp1, pg_namespace nsp2
+                WHERE c1.oid = con.conrelid
+                    AND nsp1.oid = c1.relnamespace
+                    AND c2.oid = con.confrelid
+                    AND nsp2.oid = c2.relnamespace
+                    AND nsp1.nspname = %s
+                    AND c1.relname = %s
+                    AND con.contype = 'f'""",
+                [qname.schema, qname.table])
         relations = {}
         for row in cursor.fetchall():
             # row[0] and row[1] are single-item lists, so grab the single item.
-            relations[row[0][0] - 1] = (row[1][0] - 1, row[2])
+            relations[row[0][0] - 1] = (row[1][0] - 1,
+                                        QName(row[2], row[3], True))
         return relations
 
-    def get_indexes(self, cursor, table_name):
+    def get_indexes(self, cursor, qname):
         """
         Returns a dictionary of fieldname -> infodict for the given table,
         where each infodict is in the format:
@@ -73,15 +135,31 @@
         """
         # This query retrieves each index on the given table, including the
         # first associated field name
-        cursor.execute("""
-            SELECT attr.attname, idx.indkey, idx.indisunique, idx.indisprimary
-            FROM pg_catalog.pg_class c, pg_catalog.pg_class c2,
-                pg_catalog.pg_index idx, pg_catalog.pg_attribute attr
-            WHERE c.oid = idx.indrelid
-                AND idx.indexrelid = c2.oid
-                AND attr.attrelid = c.oid
-                AND attr.attnum = idx.indkey[0]
-                AND c.relname = %s""", [table_name])
+        qname = self.qname_converter(qname)
+        if not qname.schema:
+            cursor.execute("""
+                SELECT attr.attname, idx.indkey, idx.indisunique, idx.indisprimary
+                FROM pg_catalog.pg_class c, pg_catalog.pg_class c2,
+                    pg_catalog.pg_index idx, pg_catalog.pg_attribute attr
+                WHERE c.oid = idx.indrelid
+                    AND idx.indexrelid = c2.oid
+                    AND attr.attrelid = c.oid
+                    AND attr.attnum = idx.indkey[0]
+                    AND c.relname = %s""",
+                [qname.table])
+        else:
+            cursor.execute("""
+                SELECT attr.attname, idx.indkey, idx.indisunique, idx.indisprimary
+                FROM pg_catalog.pg_class c, pg_catalog.pg_namespace nsp,
+                    pg_catalog.pg_class c2, pg_catalog.pg_index idx,
+                    pg_catalog.pg_attribute attr
+                WHERE c.oid = idx.indrelid
+                    AND nsp.oid = c.relnamespace
+                    AND idx.indexrelid = c2.oid
+                    AND attr.attrelid = c.oid
+                    AND attr.attnum = idx.indkey[0]
+                    AND nsp.nspname = %s AND c.relname = %s""",
+                [qname.schema, qname.table])
         indexes = {}
         for row in cursor.fetchall():
             # row[1] (idx.indkey) is stored in the DB as an array. It comes out as
@@ -92,3 +170,15 @@
                 continue
             indexes[row[0]] = {'primary_key': row[3], 'unique': row[2]}
         return indexes
+
+    def qname_converter(self, qname, force_schema=False):
+        """
+        On postgresql it is impossible to force usage of schema - 
+        we do not know what the default schema is. In fact, there can be
+        multiple schemas.
+        """
+        assert isinstance(qname, QName)
+        if qname.db_format:
+            return qname
+        return QName((qname.schema or self.connection.schema), qname.table,
+                     True)
Index: django/db/backends/postgresql_psycopg2/operations.py
===================================================================
--- django/db/backends/postgresql_psycopg2/operations.py	(revision 17914)
+++ django/db/backends/postgresql_psycopg2/operations.py	(working copy)
@@ -1,3 +1,4 @@
+from django.db import QName
 from django.db.backends import BaseDatabaseOperations
 
 
@@ -56,11 +57,11 @@
             return 'HOST(%s)'
         return '%s'
 
-    def last_insert_id(self, cursor, table_name, pk_name):
+    def last_insert_id(self, cursor, qualified_name, pk_name):
         # Use pg_get_serial_sequence to get the underlying sequence name
         # from the table name and column name (available since PostgreSQL 8)
         cursor.execute("SELECT CURRVAL(pg_get_serial_sequence('%s','%s'))" % (
-            self.quote_name(table_name), pk_name))
+            self.qualified_name(qualified_name), pk_name))
         return cursor.fetchone()[0]
 
     def no_limit_value(self):
@@ -71,6 +72,25 @@
             return name # Quoting once is enough.
         return '"%s"' % name
 
+    def qualified_name(self, qname):
+        """
+        Return the table's name in fully qualified format
+        (schema_name.tbl_name) if there is a schema_name, else returns just
+        the tbl_name in quoted format.
+
+        convert_name has no effect on PostgreSQL, as there is no need
+        to do anything else than quoting for the name even in testing.
+        """
+        assert isinstance(qname, QName)
+        schema = qname.schema
+        if not qname.db_format and not schema:
+            schema = self.connection.schema
+        if schema:
+            return "%s.%s" % (self.quote_name(schema),
+                              self.quote_name(qname.table))
+        else:
+            return self.quote_name(qname.table)
+
     def set_time_zone_sql(self):
         return "SET TIME ZONE %s"
 
@@ -81,13 +101,15 @@
             # table.
             sql = ['%s %s;' % \
                 (style.SQL_KEYWORD('TRUNCATE'),
-                    style.SQL_FIELD(', '.join([self.quote_name(table) for table in tables]))
+                    style.SQL_FIELD(', '.join([self.qualified_name(table)
+                                               for table in tables]))
             )]
 
             # 'ALTER SEQUENCE sequence_name RESTART WITH 1;'... style SQL statements
             # to reset sequence indices
             for sequence_info in sequences:
-                table_name = sequence_info['table']
+                qname = sequence_info['qname']
+                qualified_name = self.qualified_name(qname)
                 column_name = sequence_info['column']
                 if not (column_name and len(column_name) > 0):
                     # This will be the case if it's an m2m using an autogenerated
@@ -95,7 +117,7 @@
                     column_name = 'id'
                 sql.append("%s setval(pg_get_serial_sequence('%s','%s'), 1, false);" % \
                     (style.SQL_KEYWORD('SELECT'),
-                    style.SQL_TABLE(self.quote_name(table_name)),
+                    style.SQL_TABLE(qualified_name),
                     style.SQL_FIELD(column_name))
                 )
             return sql
@@ -121,27 +143,29 @@
 
             for f in model._meta.local_fields:
                 if isinstance(f, models.AutoField):
+                    qualified_name = self.qualified_name(model._meta.qualified_name)
                     output.append("%s setval(pg_get_serial_sequence('%s','%s'), coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \
                         (style.SQL_KEYWORD('SELECT'),
-                        style.SQL_TABLE(qn(model._meta.db_table)),
+                        style.SQL_TABLE(qualified_name),
                         style.SQL_FIELD(f.column),
                         style.SQL_FIELD(qn(f.column)),
                         style.SQL_FIELD(qn(f.column)),
                         style.SQL_KEYWORD('IS NOT'),
                         style.SQL_KEYWORD('FROM'),
-                        style.SQL_TABLE(qn(model._meta.db_table))))
+                        style.SQL_TABLE(qualified_name)))
                     break # Only one AutoField is allowed per model, so don't bother continuing.
             for f in model._meta.many_to_many:
                 if not f.rel.through:
+                    qualified_name = self.qualified_name(f.m2m_qualified_name())
                     output.append("%s setval(pg_get_serial_sequence('%s','%s'), coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \
                         (style.SQL_KEYWORD('SELECT'),
-                        style.SQL_TABLE(qn(f.m2m_db_table())),
+                        style.SQL_TABLE(qualified_name),
                         style.SQL_FIELD('id'),
                         style.SQL_FIELD(qn('id')),
                         style.SQL_FIELD(qn('id')),
                         style.SQL_KEYWORD('IS NOT'),
                         style.SQL_KEYWORD('FROM'),
-                        style.SQL_TABLE(qn(f.m2m_db_table()))))
+                        style.SQL_TABLE(qualified_name)))
         return output
 
     def savepoint_create_sql(self, sid):
Index: django/db/backends/postgresql_psycopg2/creation.py
===================================================================
--- django/db/backends/postgresql_psycopg2/creation.py	(revision 17914)
+++ django/db/backends/postgresql_psycopg2/creation.py	(working copy)
@@ -43,7 +43,9 @@
     def sql_indexes_for_field(self, model, f, style):
         if f.db_index and not f.unique:
             qn = self.connection.ops.quote_name
-            db_table = model._meta.db_table
+            qn3 = self.connection.ops.qualified_name
+            qualified_name = model._meta.qualified_name
+            db_table = qualified_name[1]
             tablespace = f.db_tablespace or model._meta.db_tablespace
             if tablespace:
                 tablespace_sql = self.connection.ops.tablespace_sql(tablespace)
@@ -56,7 +58,7 @@
                 return (style.SQL_KEYWORD('CREATE INDEX') + ' ' +
                         style.SQL_TABLE(qn(truncate_name(index_name,self.connection.ops.max_name_length()))) + ' ' +
                         style.SQL_KEYWORD('ON') + ' ' +
-                        style.SQL_TABLE(qn(db_table)) + ' ' +
+                        style.SQL_TABLE(qn3(qualified_name)) + ' ' +
                         "(%s%s)" % (style.SQL_FIELD(qn(f.column)), opclass) +
                         "%s;" % tablespace_sql)
 
@@ -85,3 +87,7 @@
         self.connection.connection.rollback()
         self.connection.connection.set_isolation_level(
                 psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
+
+    def sql_destroy_schema(self, schema, style):
+        qn = self.connection.ops.quote_name
+        return "%s %s CASCADE;" % (style.SQL_KEYWORD('DROP SCHEMA'), qn(schema))
Index: django/db/backends/postgresql_psycopg2/base.py
===================================================================
--- django/db/backends/postgresql_psycopg2/base.py	(revision 17914)
+++ django/db/backends/postgresql_psycopg2/base.py	(working copy)
@@ -83,6 +83,7 @@
     has_bulk_insert = True
     supports_tablespaces = True
     can_distinct_on_fields = True
+    namespaced_schemas = True
 
 class DatabaseWrapper(BaseDatabaseWrapper):
     vendor = 'postgresql'
@@ -121,6 +122,10 @@
         self.validation = BaseDatabaseValidation(self)
         self._pg_version = None
 
+    def _get_test_schema_prefix(self):
+        return ''
+    test_schema_prefix = property(_get_test_schema_prefix)
+
     def check_constraints(self, table_names=None):
         """
         To check constraints, we set constraints to immediate. Then, when, we're done we must ensure they
Index: django/db/backends/creation.py
===================================================================
--- django/db/backends/creation.py	(revision 17914)
+++ django/db/backends/creation.py	(working copy)
@@ -2,7 +2,9 @@
 import time
 
 from django.conf import settings
+from django.db import QName
 from django.db.utils import load_backend
+from django.core.management.color import no_style
 
 # The prefix to put on the default database name when creating
 # the test database.
@@ -28,6 +30,14 @@
         """
         return '%x' % (abs(hash(args)) % 4294967296L)  # 2**32
 
+    def sql_create_schema(self, schema, style):
+        """
+        Returns the SQL required to create a single schema
+        """
+        qn = self.connection.ops.quote_name
+        output = "%s %s;" % (style.SQL_KEYWORD('CREATE SCHEMA'), qn(schema))
+        return output
+
     def sql_create_model(self, model, style, known_models=set()):
         """
         Returns the SQL required to create a single model, as a tuple of:
@@ -40,6 +50,7 @@
         table_output = []
         pending_references = {}
         qn = self.connection.ops.quote_name
+        qn3 = self.connection.ops.qualified_name
         for f in opts.local_fields:
             col_type = f.db_type(connection=self.connection)
             tablespace = f.db_tablespace or opts.db_tablespace
@@ -79,7 +90,7 @@
                      for f in field_constraints]))
 
         full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' +
-                          style.SQL_TABLE(qn(opts.db_table)) + ' (']
+                          style.SQL_TABLE(qn3(opts.qualified_name)) + ' (']
         for i, line in enumerate(table_output): # Combine and add commas.
             full_statement.append(
                 '    %s%s' % (line, i < len(table_output)-1 and ',' or ''))
@@ -96,7 +107,7 @@
             # Add any extra SQL needed to support auto-incrementing primary
             # keys.
             auto_column = opts.auto_field.db_column or opts.auto_field.name
-            autoinc_sql = self.connection.ops.autoinc_sql(opts.db_table,
+            autoinc_sql = self.connection.ops.autoinc_sql(opts.qualified_name,
                                                           auto_column)
             if autoinc_sql:
                 for stmt in autoinc_sql:
@@ -109,9 +120,12 @@
         Return the SQL snippet defining the foreign key reference for a field.
         """
         qn = self.connection.ops.quote_name
+        from_qname = field.model._meta.qualified_name
+        to_qname = field.rel.to._meta.qualified_name
+        qname = self.qualified_name_for_ref(from_qname, to_qname)
         if field.rel.to in known_models:
             output = [style.SQL_KEYWORD('REFERENCES') + ' ' +
-                style.SQL_TABLE(qn(field.rel.to._meta.db_table)) + ' (' +
+                style.SQL_TABLE(qname) + ' (' +
                 style.SQL_FIELD(qn(field.rel.to._meta.get_field(
                     field.rel.field_name).column)) + ')' +
                 self.connection.ops.deferrable_sql()
@@ -132,16 +146,24 @@
         from django.db.backends.util import truncate_name
 
         if not model._meta.managed or model._meta.proxy:
+            # So, we have a reference to either unmanaged model or to
+            # a proxy model. Lets just clear the pending_references
+            # for now.
+            if model in pending_references:
+                del pending_references[model]
             return []
         qn = self.connection.ops.quote_name
+        qn3 = self.connection.ops.qualified_name
         final_output = []
         opts = model._meta
         if model in pending_references:
             for rel_class, f in pending_references[model]:
                 rel_opts = rel_class._meta
                 r_table = rel_opts.db_table
+                r_qname = rel_opts.qualified_name
                 r_col = f.column
                 table = opts.db_table
+                qname = self.qualified_name_for_ref(r_qname, opts.qualified_name)
                 col = opts.get_field(f.rel.field_name).column
                 # For MySQL, r_name must be unique in the first 64 characters.
                 # So we are careful with character usage here.
@@ -149,13 +171,23 @@
                     r_col, col, self._digest(r_table, table))
                 final_output.append(style.SQL_KEYWORD('ALTER TABLE') +
                     ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' %
-                    (qn(r_table), qn(truncate_name(
+                    (qn3(r_qname), qn(truncate_name(
                         r_name, self.connection.ops.max_name_length())),
-                    qn(r_col), qn(table), qn(col),
+                    qn(r_col), qname, qn(col),
                     self.connection.ops.deferrable_sql()))
             del pending_references[model]
         return final_output
 
+    def qualified_name_for_ref(self, from_table, ref_table):
+        """
+        In certain databases if the from_table is in qualified format and
+        ref_table is not, it is assumed the ref_table references a table
+        in the same schema as from_table is from. However, we want the
+        reference to be to default schema, not the same schema the from_table
+        is. This method will fix this issue where that is a problem.
+        """
+        return self.connection.ops.qualified_name(ref_table)
+
     def sql_indexes_for_model(self, model, style):
         """
         Returns the CREATE INDEX SQL statements for a single model.
@@ -171,10 +203,9 @@
         """
         Return the CREATE INDEX SQL statements for a single model field.
         """
-        from django.db.backends.util import truncate_name
-
         if f.db_index and not f.unique:
             qn = self.connection.ops.quote_name
+            qn3 = self.connection.ops.qualified_name
             tablespace = f.db_tablespace or model._meta.db_tablespace
             if tablespace:
                 tablespace_sql = self.connection.ops.tablespace_sql(tablespace)
@@ -182,18 +213,36 @@
                     tablespace_sql = ' ' + tablespace_sql
             else:
                 tablespace_sql = ''
-            i_name = '%s_%s' % (model._meta.db_table, self._digest(f.column))
+            qualified_name = self.qualified_index_name(model, f.column) 
             output = [style.SQL_KEYWORD('CREATE INDEX') + ' ' +
-                style.SQL_TABLE(qn(truncate_name(
-                    i_name, self.connection.ops.max_name_length()))) + ' ' +
+                style.SQL_TABLE(qualified_name) + ' ' +
                 style.SQL_KEYWORD('ON') + ' ' +
-                style.SQL_TABLE(qn(model._meta.db_table)) + ' ' +
+                style.SQL_TABLE(qn3(model._meta.qualified_name)) + ' ' +
                 "(%s)" % style.SQL_FIELD(qn(f.column)) +
                 "%s;" % tablespace_sql]
         else:
             output = []
         return output
 
+    def qualified_index_name(self, model, col):
+        """
+        Some databases do support schemas, but indexes can not be placed in a
+        different schema. So, to support those databases, we need to be able
+        to return the index name in different qualified format than the rest
+        of the database identifiers.
+        """
+        from django.db.backends.util import truncate_name
+        i_name = '%s_%s' % (model._meta.db_table, self._digest(col))
+        i_name = truncate_name(i_name, self.connection.ops.max_name_length())
+        return self.connection.ops.qualified_name(
+            QName(model._meta.db_schema, i_name, False))
+
+    def sql_destroy_schema(self, schema, style):
+        """
+        Returns the SQL required to destroy a single schema.
+        """
+        return ""
+
     def sql_destroy_model(self, model, references_to_delete, style):
         """
         Return the DROP TABLE and restraint dropping statements for a single
@@ -202,14 +251,14 @@
         if not model._meta.managed or model._meta.proxy:
             return []
         # Drop the table now
-        qn = self.connection.ops.quote_name
+        qn3 = self.connection.ops.qualified_name
         output = ['%s %s;' % (style.SQL_KEYWORD('DROP TABLE'),
-                              style.SQL_TABLE(qn(model._meta.db_table)))]
+                              style.SQL_TABLE(qn3(model._meta.qualified_name)))]
         if model in references_to_delete:
             output.extend(self.sql_remove_table_constraints(
                 model, references_to_delete, style))
         if model._meta.has_auto_field:
-            ds = self.connection.ops.drop_sequence_sql(model._meta.db_table)
+            ds = self.connection.ops.drop_sequence_sql(model._meta.qualified_name)
             if ds:
                 output.append(ds)
         return output
@@ -220,8 +269,10 @@
             return []
         output = []
         qn = self.connection.ops.quote_name
+        qn3 = self.connection.ops.qualified_name
         for rel_class, f in references_to_delete[model]:
             table = rel_class._meta.db_table
+            qname = rel_class._meta.qualified_name
             col = f.column
             r_table = model._meta.db_table
             r_col = model._meta.get_field(f.rel.field_name).column
@@ -229,7 +280,7 @@
                 col, r_col, self._digest(table, r_table))
             output.append('%s %s %s %s;' % \
                 (style.SQL_KEYWORD('ALTER TABLE'),
-                style.SQL_TABLE(qn(table)),
+                style.SQL_TABLE(qn3(qname)),
                 style.SQL_KEYWORD(self.connection.ops.drop_foreignkey_sql()),
                 style.SQL_FIELD(qn(truncate_name(
                     r_name, self.connection.ops.max_name_length())))))
@@ -240,6 +291,10 @@
         """
         Creates a test database, prompting the user for confirmation if the
         database already exists. Returns the name of the test database created.
+
+        Also creates needed schemas, which on some backends live in the same
+        namespace than databases. If there are schema name clashes, prompts
+        the user for confirmation.
         """
         # Don't import django.core.management if it isn't needed.
         from django.core.management import call_command
@@ -253,10 +308,14 @@
             print "Creating test database for alias '%s'%s..." % (
                 self.connection.alias, test_db_repr)
 
-        self._create_test_db(verbosity, autoclobber)
+        schemas = self.get_schemas()
+        self._create_test_db(verbosity, autoclobber, schemas)
 
+        # Create the test schemas.
+        self.connection.settings_dict["NAME"] = test_database_name
         self.connection.close()
-        self.connection.settings_dict["NAME"] = test_database_name
+        schemas = ['%s%s' % (self.connection.test_schema_prefix, s) for s in schemas]
+        created_schemas = self._create_test_schemas(verbosity, schemas, autoclobber)
 
         # Confirm the feature set of the test database
         self.connection.features.confirm()
@@ -292,8 +351,61 @@
         # the side effect of initializing the test database.
         self.connection.cursor()
 
-        return test_database_name
+        return test_database_name, created_schemas
 
+    def _create_test_schemas(self, verbosity, schemas, autoclobber):
+        style = no_style()
+        cursor = self.connection.cursor()
+        existing_schemas = self.connection.introspection.get_schema_list(cursor)
+        if not self.connection.features.namespaced_schemas:
+            conflicts = [s for s in existing_schemas if s in schemas]
+        else:
+            conflicts = []
+        if conflicts:
+            print 'The following schemas already exists: %s' % ', '.join(conflicts) 
+            if not autoclobber:
+                confirm = raw_input(
+                    "Type 'yes' if you would like to try deleting these schemas "
+                    "or 'no' to cancel: ")
+            if autoclobber or confirm == 'yes':
+                try:
+                    # Some databases (well, MySQL) complain about foreign keys when
+                    # dropping a database. So, disable the constraints temporarily.
+                    self.connection.disable_constraint_checking()
+                    for schema in conflicts:
+                        if verbosity >= 1:
+                            print "Destroying schema %s" % schema
+                        cursor.execute(self.sql_destroy_schema(schema, style))
+                        existing_schemas.remove(schema)
+                finally:
+                    self.connection.enable_constraint_checking()
+            else:
+                print "Tests cancelled."
+                sys.exit(1)
+           
+        to_create = [s for s in schemas if s not in existing_schemas]
+        for schema in to_create:
+            if verbosity >= 1:
+                print "Creating schema %s" % schema
+            cursor.execute(self.sql_create_schema(schema, style))
+            self.connection.settings_dict['TEST_SCHEMAS'].append(schema)
+        return to_create
+
+    def get_schemas(self):
+        from django.db import models
+        apps = models.get_apps()
+        schemas = set()
+        for app in apps:
+            app_models = models.get_models(app, include_auto_created=True)
+            for model in app_models:
+                schema = model._meta.db_schema
+                if schema:
+                    schemas.add(schema)
+        conn_default_schema = self.connection.settings_dict['SCHEMA']
+        if conn_default_schema:
+            schemas.add(conn_default_schema)
+        return schemas
+
     def _get_test_db_name(self):
         """
         Internal implementation - returns the name of the test DB that will be
@@ -305,7 +417,7 @@
             return self.connection.settings_dict['TEST_NAME']
         return TEST_DATABASE_PREFIX + self.connection.settings_dict['NAME']
 
-    def _create_test_db(self, verbosity, autoclobber):
+    def _create_test_db(self, verbosity, autoclobber, schemas):
         """
         Internal implementation - creates the test db tables.
         """
@@ -335,9 +447,19 @@
                     if verbosity >= 1:
                         print ("Destroying old test database '%s'..."
                                % self.connection.alias)
+                    # MySQL nicely doesn't have a drop-cascade option, nor
+                    # does it allow dropping a database having foreign key
+                    # references pointing to it. So, we just disable foreign
+                    # key checks and then immediately enable them. MySQL is
+                    # happy after this hack, and other databases simply do
+                    # not care.
+                    try:
+                        self.connection.disable_constraint_checking()
+                        cursor.execute(
+                            "DROP DATABASE %s" % qn(test_database_name))
+                    finally:
+                        self.connection.enable_constraint_checking()
                     cursor.execute(
-                        "DROP DATABASE %s" % qn(test_database_name))
-                    cursor.execute(
                         "CREATE DATABASE %s %s" % (qn(test_database_name),
                                                    suffix))
                 except Exception, e:
@@ -348,15 +470,32 @@
                 print "Tests cancelled."
                 sys.exit(1)
 
+        self.connection.settings_dict['TEST_SCHEMAS'].append(test_database_name)
         return test_database_name
 
-    def destroy_test_db(self, old_database_name, verbosity=1):
+    def destroy_test_db(self, old_database_name, created_schemas, verbosity=1):
         """
         Destroy a test database, prompting the user for confirmation if the
         database already exists.
         """
+        # On databases where there is no support for multiple databases
+        # with multiple schemas we need to destroy the created schemas
+        # manually.
+        cursor = self.connection.cursor()
+        style = no_style()
+        if not self.connection.features.namespaced_schemas:
+            try:
+                self.connection.disable_constraint_checking()
+                for schema in created_schemas:
+                    if verbosity >= 1:
+                        print "Destroying schema '%s'..." % schema
+                    cursor.execute(self.sql_destroy_schema(schema, style))
+            finally:
+                self.connection.enable_constraint_checking()
         self.connection.close()
         test_database_name = self.connection.settings_dict['NAME']
+                    
+
         if verbosity >= 1:
             test_db_repr = ''
             if verbosity >= 2:
@@ -429,3 +568,11 @@
             settings_dict['ENGINE'],
             settings_dict['NAME']
         )
+
+    def post_create_pending_references(self, pending_references, as_sql=False):
+        """
+        Create any pending references which need special handling (for example
+        different connections). The as_sql flag tells us if we should return
+        the raw SQL used. This is needed for the "sql" management commands.
+        """
+        raise NotImplementedError
Index: django/db/backends/sqlite3/base.py
===================================================================
--- django/db/backends/sqlite3/base.py	(revision 17914)
+++ django/db/backends/sqlite3/base.py	(working copy)
@@ -11,7 +11,7 @@
 import re
 import sys
 
-from django.db import utils
+from django.db import utils, QName
 from django.db.backends import *
 from django.db.backends.signals import connection_created
 from django.db.backends.sqlite3.client import DatabaseClient
@@ -84,6 +84,11 @@
     supports_mixed_date_datetime_comparisons = False
     has_bulk_insert = True
     can_combine_inserts_with_and_without_auto_increment_pk = True
+    supports_foreign_keys = False
+    # SQLite doesn't support schemas at all, but our hack of appending
+    # the schema name to table name creates namespaced schemas from
+    # Django's perspective
+    namespaced_schemas = True
 
     def _supports_stddev(self):
         """Confirm support for STDDEV and related stats functions
@@ -139,6 +144,22 @@
             return name # Quoting once is enough.
         return '"%s"' % name
 
+    def qualified_name(self, qname):
+        # Fake schema support by using the schema as a prefix to the
+        # table name. Keep record of what names are already qualified
+        # to avoid double-qualifying.
+        assert isinstance(qname, QName)
+        if qname.db_format:
+            # A name from DB must not have a schema (no schema support)
+            assert not qname.schema
+            schema = None
+        else:
+            schema = qname.schema or self.connection.schema
+        if schema:
+            return self.quote_name('%s_%s' % (schema, qname.table))
+        else:
+            return self.quote_name(qname.table)
+
     def no_limit_value(self):
         return -1
 
@@ -146,11 +167,13 @@
         # NB: The generated SQL below is specific to SQLite
         # Note: The DELETE FROM... SQL generated below works for SQLite databases
         # because constraints don't exist
-        sql = ['%s %s %s;' % \
+        sql = []
+        for table in tables:
+            sql.append('%s %s %s;' % \
                 (style.SQL_KEYWORD('DELETE'),
                  style.SQL_KEYWORD('FROM'),
-                 style.SQL_FIELD(self.quote_name(table))
-                 ) for table in tables]
+                 style.SQL_FIELD(self.qualified_name(table))
+                 ))
         # Note: No requirement for reset of auto-incremented indices (cf. other
         # sql_flush() implementations). Just return SQL at this point
         return sql
@@ -243,6 +266,10 @@
         self.introspection = DatabaseIntrospection(self)
         self.validation = BaseDatabaseValidation(self)
 
+    def convert_schema(self, schema):
+        # No real schema support.
+        return None
+
     def _sqlite_create_connection(self):
         settings_dict = self.settings_dict
         if not settings_dict['NAME']:
@@ -295,7 +322,9 @@
         """
         cursor = self.cursor()
         if table_names is None:
-            table_names = self.introspection.get_table_list(cursor)
+            table_names = self.introspection.get_visible_tables_list(cursor)
+        else:
+            table_names = [self.introspection.qname_converter(t) for t in table_names]
         for table_name in table_names:
             primary_key_column_name = self.introspection.get_primary_key_column(cursor, table_name)
             if not primary_key_column_name:
@@ -307,12 +336,12 @@
                     LEFT JOIN `%s` as REFERRED
                     ON (REFERRING.`%s` = REFERRED.`%s`)
                     WHERE REFERRING.`%s` IS NOT NULL AND REFERRED.`%s` IS NULL"""
-                    % (primary_key_column_name, column_name, table_name, referenced_table_name,
+                    % (primary_key_column_name, column_name, table_name[1], referenced_table_name[1],
                     column_name, referenced_column_name, column_name, referenced_column_name))
                 for bad_row in cursor.fetchall():
                     raise utils.IntegrityError("The row in table '%s' with primary key '%s' has an invalid "
                         "foreign key: %s.%s contains a value '%s' that does not have a corresponding value in %s.%s."
-                        % (table_name, bad_row[0], table_name, column_name, bad_row[1],
+                        % (table_name, bad_row[0], table_name[1], column_name, bad_row[1],
                         referenced_table_name, referenced_column_name))
 
     def close(self):
Index: django/db/backends/sqlite3/introspection.py
===================================================================
--- django/db/backends/sqlite3/introspection.py	(revision 17914)
+++ django/db/backends/sqlite3/introspection.py	(working copy)
@@ -1,4 +1,5 @@
 import re
+from django.db import QName
 from django.db.backends import BaseDatabaseIntrospection
 
 # This light wrapper "fakes" a dictionary interface, because some SQLite data
@@ -41,26 +42,27 @@
 class DatabaseIntrospection(BaseDatabaseIntrospection):
     data_types_reverse = FlexibleFieldLookupDict()
 
-    def get_table_list(self, cursor):
-        "Returns a list of table names in the current database."
+    def get_visible_tables_list(self, cursor):
+        "Returns a list of table names in the current database"
         # Skip the sqlite_sequence system table used for autoincrement key
         # generation.
         cursor.execute("""
             SELECT name FROM sqlite_master
             WHERE type='table' AND NOT name='sqlite_sequence'
             ORDER BY name""")
-        return [row[0] for row in cursor.fetchall()]
+        return [QName(None, row[0], True) for row in cursor.fetchall()]
 
-    def get_table_description(self, cursor, table_name):
+    def get_table_description(self, cursor, qualified_name):
         "Returns a description of the table, with the DB-API cursor.description interface."
         return [(info['name'], info['type'], None, None, None, None,
-                 info['null_ok']) for info in self._table_info(cursor, table_name)]
+                 info['null_ok']) for info in self._table_info(cursor, qualified_name[1])]
 
-    def get_relations(self, cursor, table_name):
+    def get_relations(self, cursor, qualified_name):
         """
         Returns a dictionary of {field_index: (field_index_other_table, other_table)}
         representing all relationships to the given table. Indexes are 0-based.
         """
+        table_name = qualified_name[1]
 
         # Dictionary of relations to return
         relations = {}
@@ -98,16 +100,18 @@
 
                 name = other_desc.split(' ', 1)[0].strip('"')
                 if name == column:
-                    relations[field_index] = (other_index, table)
+                    relations[field_index] = (other_index,
+                                              QName(None, table, True))
                     break
 
         return relations
 
-    def get_key_columns(self, cursor, table_name):
+    def get_key_columns(self, cursor, qname):
         """
         Returns a list of (column_name, referenced_table_name, referenced_column_name) for all
         key columns in given table.
         """
+        table_name = self.qname_converter(qname)[1]
         key_columns = []
 
         # Schema for this table
@@ -128,17 +132,20 @@
                 continue
 
             # This will append (column_name, referenced_table_name, referenced_column_name) to key_columns
-            key_columns.append(tuple([s.strip('"') for s in m.groups()]))
+            add = tuple([s.strip('"') for s in m.groups()])
+            add = add[0], (None, add[1], True), add[2]
+            key_columns.append(add)
 
         return key_columns
 
-    def get_indexes(self, cursor, table_name):
+    def get_indexes(self, cursor, qualified_name):
         """
         Returns a dictionary of fieldname -> infodict for the given table,
         where each infodict is in the format:
             {'primary_key': boolean representing whether it's the primary key,
              'unique': boolean representing whether it's a unique index}
         """
+        table_name = qualified_name[1]
         indexes = {}
         for info in self._table_info(cursor, table_name):
             indexes[info['name']] = {'primary_key': info['pk'] != 0,
@@ -157,10 +164,12 @@
             indexes[name]['unique'] = True
         return indexes
 
-    def get_primary_key_column(self, cursor, table_name):
+    def get_primary_key_column(self, cursor, qname):
         """
         Get the column name of the primary key for the given table.
         """
+        qname = self.qname_converter(qname)
+        table_name = qname[1]
         # Don't use PRAGMA because that causes issues with some transactions
         cursor.execute("SELECT sql FROM sqlite_master WHERE tbl_name = %s AND type = %s", [table_name, "table"])
         results = cursor.fetchone()[0].strip()
@@ -180,3 +189,14 @@
                  'null_ok': not field[3],
                  'pk': field[5]     # undocumented
                  } for field in cursor.fetchall()]
+
+    def identifier_converter(self, identifier):
+        return identifier
+
+    def qname_converter(self, qname, force_schema=False):
+        # For SQLite force_schema does nothing, as the default schema is
+        # None.
+        assert isinstance(qname, QName)
+        assert not (qname.schema and qname.db_format)
+        return QName(None, self.connection.ops.qualified_name(qname)[1:-1],
+                     True)
Index: django/db/backends/sqlite3/creation.py
===================================================================
--- django/db/backends/sqlite3/creation.py	(revision 17914)
+++ django/db/backends/sqlite3/creation.py	(working copy)
@@ -33,19 +33,25 @@
 
     def sql_for_pending_references(self, model, style, pending_references):
         "SQLite3 doesn't support constraints"
+        if model in pending_references:
+            del pending_references[model]
         return []
 
     def sql_remove_table_constraints(self, model, references_to_delete, style):
         "SQLite3 doesn't support constraints"
         return []
 
+    def sql_create_schema(self, schema, verbosity):
+        "SQLite3 doesn't support schemas"
+        return
+
     def _get_test_db_name(self):
         test_database_name = self.connection.settings_dict['TEST_NAME']
         if test_database_name and test_database_name != ':memory:':
             return test_database_name
         return ':memory:'
 
-    def _create_test_db(self, verbosity, autoclobber):
+    def _create_test_db(self, verbosity, autoclobber, schemas):
         test_database_name = self._get_test_db_name()
         if test_database_name != ':memory:':
             # Erase the old test database
@@ -65,6 +71,9 @@
                     sys.exit(1)
         return test_database_name
 
+    def _create_test_schemas(self, verbosity, schemas, cursor):
+        return []
+
     def _destroy_test_db(self, test_database_name, verbosity):
         if test_database_name and test_database_name != ":memory:":
             # Remove the SQLite database file
@@ -81,7 +90,6 @@
         SQLite since the databases will be distinct despite having the same
         TEST_NAME. See http://www.sqlite.org/inmemorydb.html
         """
-        settings_dict = self.connection.settings_dict
         test_dbname = self._get_test_db_name()
         sig = [self.connection.settings_dict['NAME']]
         if test_dbname == ':memory:':
Index: django/db/backends/mysql/introspection.py
===================================================================
--- django/db/backends/mysql/introspection.py	(revision 17914)
+++ django/db/backends/mysql/introspection.py	(working copy)
@@ -1,3 +1,4 @@
+from django.db import QName
 from django.db.backends import BaseDatabaseIntrospection
 from MySQLdb import ProgrammingError, OperationalError
 from MySQLdb.constants import FIELD_TYPE
@@ -28,30 +29,44 @@
         FIELD_TYPE.VAR_STRING: 'CharField',
     }
 
-    def get_table_list(self, cursor):
-        "Returns a list of table names in the current database."
-        cursor.execute("SHOW TABLES")
-        return [row[0] for row in cursor.fetchall()]
+    def get_visible_tables_list(self, cursor):
+        "Returns a list of visible tables"
+        return self.get_qualified_tables_list(cursor, [self.connection.settings_dict['NAME']])
 
-    def get_table_description(self, cursor, table_name):
+    def get_qualified_tables_list(self, cursor, schemas):
+        default_schema = self.connection.convert_schema(None)
+        if default_schema:
+            schemas.append(default_schema)
+        if not schemas:
+            return []
+        param_list = ', '.join(['%s']*len(schemas))
+        cursor.execute("""
+            SELECT table_schema, table_name
+              FROM information_schema.tables
+             WHERE table_schema in (%s)""" % param_list, schemas)
+        return [QName(row[0], row[1], True) for row in cursor.fetchall()]
+
+    def get_table_description(self, cursor, qname):
         "Returns a description of the table, with the DB-API cursor.description interface."
-        cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name))
+        qname = self.qname_converter(qname)
+        cursor.execute("SELECT * FROM %s LIMIT 1"
+                       % self.connection.ops.qualified_name(qname))
         return cursor.description
 
-    def _name_to_index(self, cursor, table_name):
+    def _name_to_index(self, cursor, qname):
         """
         Returns a dictionary of {field_name: field_index} for the given table.
         Indexes are 0-based.
         """
-        return dict([(d[0], i) for i, d in enumerate(self.get_table_description(cursor, table_name))])
+        return dict((d[0], i) for i, d in enumerate(self.get_table_description(cursor, qname)))
 
-    def get_relations(self, cursor, table_name):
+    def get_relations(self, cursor, qname):
         """
         Returns a dictionary of {field_index: (field_index_other_table, other_table)}
         representing all relationships to the given table. Indexes are 0-based.
         """
-        my_field_dict = self._name_to_index(cursor, table_name)
-        constraints = self.get_key_columns(cursor, table_name)
+        my_field_dict = self._name_to_index(cursor, qname)
+        constraints = self.get_key_columns(cursor, qname)
         relations = {}
         for my_fieldname, other_table, other_field in constraints:
             other_field_index = self._name_to_index(cursor, other_table)[other_field]
@@ -59,25 +74,32 @@
             relations[my_field_index] = (other_field_index, other_table)
         return relations
 
-    def get_key_columns(self, cursor, table_name):
+    def get_key_columns(self, cursor, qname):
         """
-        Returns a list of (column_name, referenced_table_name, referenced_column_name) for all
-        key columns in given table.
+        Returns a list of
+            (column_name,
+            (reference_table_schema, referenced_table_name),
+            referenced_column_name)
+        for all key columns in given table.
         """
         key_columns = []
+        qname = self.qname_converter(qname, force_schema=True)
         try:
             cursor.execute("""
-                SELECT column_name, referenced_table_name, referenced_column_name
+                SELECT column_name, referenced_table_schema, referenced_table_name, referenced_column_name
                 FROM information_schema.key_column_usage
-                WHERE table_name = %s
-                    AND table_schema = DATABASE()
+                WHERE table_schema = %s
+                    AND table_name = %s
                     AND referenced_table_name IS NOT NULL
-                    AND referenced_column_name IS NOT NULL""", [table_name])
-            key_columns.extend(cursor.fetchall())
+                    AND referenced_column_name IS NOT NULL""",
+                           [qname.schema, qname.table])
+            for row in cursor.fetchall():
+                key_columns.append((row[0], QName(row[1], row[2], True),
+                                    row[3]))
         except (ProgrammingError, OperationalError):
             # Fall back to "SHOW CREATE TABLE", for previous MySQL versions.
             # Go through all constraints and save the equal matches.
-            cursor.execute("SHOW CREATE TABLE %s" % self.connection.ops.quote_name(table_name))
+            cursor.execute("SHOW CREATE TABLE %s" % self.connection.ops.qualified_name(qname))
             for row in cursor.fetchall():
                 pos = 0
                 while True:
@@ -85,28 +107,47 @@
                     if match == None:
                         break
                     pos = match.end()
-                    key_columns.append(match.groups())
+                    groups = match.groups()
+                    tblname = groups[1]
+                    if '.' in tblname:
+                        tblname = tblname.split('.')
+                    else:
+                        tblname = None, tblname
+                    key_columns.append((groups[0], tblname, groups[2]))
         return key_columns
 
-    def get_primary_key_column(self, cursor, table_name):
+    def get_primary_key_column(self, cursor, qname):
         """
         Returns the name of the primary key column for the given table
         """
-        for column in self.get_indexes(cursor, table_name).iteritems():
+        for column in self.get_indexes(cursor, qname).iteritems():
             if column[1]['primary_key']:
                 return column[0]
         return None
 
-    def get_indexes(self, cursor, table_name):
+    def get_indexes(self, cursor, qname):
         """
         Returns a dictionary of fieldname -> infodict for the given table,
         where each infodict is in the format:
             {'primary_key': boolean representing whether it's the primary key,
              'unique': boolean representing whether it's a unique index}
         """
-        cursor.execute("SHOW INDEX FROM %s" % self.connection.ops.quote_name(table_name))
+        qname = self.qname_converter(qname)
+        cursor.execute("SHOW INDEX FROM %s" % self.connection.ops.qualified_name(qname))
         indexes = {}
         for row in cursor.fetchall():
             indexes[row[4]] = {'primary_key': (row[2] == 'PRIMARY'), 'unique': not bool(row[1])}
         return indexes
 
+    def get_schema_list(self, cursor):
+        cursor.execute("SHOW DATABASES")
+        return [r[0] for r in cursor.fetchall()]
+
+    def qname_converter(self, qname, force_schema=False):
+        assert isinstance(qname, QName)
+        if qname.db_format and (qname.schema or not force_schema):
+            return qname
+        schema = self.connection.convert_schema(qname.schema)
+        if not schema and force_schema:
+            schema = self.connection.settings_dict['NAME']
+        return QName(schema, qname.table, True)
Index: django/db/backends/mysql/creation.py
===================================================================
--- django/db/backends/mysql/creation.py	(revision 17914)
+++ django/db/backends/mysql/creation.py	(working copy)
@@ -1,3 +1,4 @@
+from django.db import QName
 from django.db.backends.creation import BaseDatabaseCreation
 
 class DatabaseCreation(BaseDatabaseCreation):
@@ -58,9 +59,41 @@
             style.SQL_KEYWORD('NOT NULL'))
         ]
         deferred = [
-            (field.m2m_db_table(), field.m2m_column_name(), opts.db_table,
+            (field.m2m_qualified_table(), field.m2m_column_name(), opts.qualified_name,
                 opts.pk.column),
-            (field.m2m_db_table(), field.m2m_reverse_name(),
-                field.rel.to._meta.db_table, field.rel.to._meta.pk.column)
+            (field.m2m_qualified_table(), field.m2m_reverse_name(),
+                field.rel.to._meta.qualified_name, field.rel.to._meta.pk.column)
             ]
         return table_output, deferred
+
+    def sql_destroy_schema(self, schema, style):
+        qn = self.connection.ops.quote_name
+        return "%s %s;" % (style.SQL_KEYWORD('DROP DATABASE'), qn(schema))
+    
+    def qualified_index_name(self, model, col):
+        """
+        On MySQL we must use the db_schema prefixed to the index name as
+        indexes can not be placed into different schemas.
+        """
+        from django.db.backends.util import truncate_name
+        schema = model._meta.db_schema or self.connection.schema
+        max_len = self.connection.ops.max_name_length()
+        schema_prefix = ''
+        if schema:
+             schema = self.connection.convert_schema(schema)
+             schema_prefix = truncate_name(schema, max_len / 2) + '_'
+        i_name = '%s%s_%s' % (schema_prefix, model._meta.db_table, self._digest(col))
+        i_name = self.connection.ops.quote_name(truncate_name(i_name, max_len))
+        return i_name
+
+    def qualified_name_for_ref(self, from_table, ref_table):
+        """
+        MySQL does not have qualified name format for indexes, so make sure to
+        use qualified names if needed.
+        """
+        from_qn = self.connection.introspection.qname_converter(from_table)
+        to_qn = self.connection.introspection.qname_converter(ref_table)
+        if to_qn.schema is None:
+            to_qn = QName(self.connection.settings_dict['NAME'],
+                          to_qn.table, to_qn.db_format)
+        return super(DatabaseCreation, self).qualified_name_for_ref(from_qn, to_qn)
Index: django/db/backends/mysql/base.py
===================================================================
--- django/db/backends/mysql/base.py	(revision 17914)
+++ django/db/backends/mysql/base.py	(working copy)
@@ -15,6 +15,8 @@
     from django.core.exceptions import ImproperlyConfigured
     raise ImproperlyConfigured("Error loading MySQLdb module: %s" % e)
 
+from django.db.backends.util import truncate_name
+
 # We want version (1, 2, 1, 'final', 2) or later. We can't just use
 # lexicographic ordering in this check because then (1, 2, 1, 'gamma')
 # inadvertently passes the version test.
@@ -187,6 +189,10 @@
         "Confirm support for introspected foreign keys"
         return self._mysql_storage_engine() != 'MyISAM'
 
+    def confirm(self):
+        super(DatabaseFeatures, self).confirm()
+        self.supports_foreign_keys != self.can_introspect_foreign_keys
+
 class DatabaseOperations(BaseDatabaseOperations):
     compiler_module = "django.db.backends.mysql.compiler"
 
@@ -245,6 +251,16 @@
             return name # Quoting once is enough.
         return "`%s`" % name
 
+    def qualified_name(self, qname):
+        schema = qname.schema
+        if not qname.db_format:
+            schema = self.connection.convert_schema(schema)
+        if schema:
+            return "%s.%s" % (self.quote_name(schema),
+                              self.quote_name(qname.table))
+        else:
+            return self.quote_name(qname.table)
+
     def random_function_sql(self):
         return 'RAND()'
 
@@ -255,7 +271,9 @@
         if tables:
             sql = ['SET FOREIGN_KEY_CHECKS = 0;']
             for table in tables:
-                sql.append('%s %s;' % (style.SQL_KEYWORD('TRUNCATE'), style.SQL_FIELD(self.quote_name(table))))
+                sql.append('%s %s;'
+                           % (style.SQL_KEYWORD('TRUNCATE'),
+                              style.SQL_FIELD(self.qualified_name(table))))
             sql.append('SET FOREIGN_KEY_CHECKS = 1;')
 
             # 'ALTER TABLE table AUTO_INCREMENT = 1;'... style SQL statements
@@ -263,7 +281,7 @@
             sql.extend(["%s %s %s %s %s;" % \
                 (style.SQL_KEYWORD('ALTER'),
                  style.SQL_KEYWORD('TABLE'),
-                 style.SQL_TABLE(self.quote_name(sequence['table'])),
+                 style.SQL_TABLE(self.qualified_name((sequence['qname']))),
                  style.SQL_KEYWORD('AUTO_INCREMENT'),
                  style.SQL_FIELD('= 1'),
                 ) for sequence in sequences])
@@ -347,6 +365,13 @@
         self.creation = DatabaseCreation(self)
         self.introspection = DatabaseIntrospection(self)
         self.validation = DatabaseValidation(self)
+    
+    def convert_schema(self, schema):
+        schema = schema or self.schema
+        if schema and self.test_schema_prefix:
+            return truncate_name('%s%s' % (self.test_schema_prefix, schema),
+                                 self.ops.max_name_length())
+        return schema
 
     def _valid_connection(self):
         if self.connection is not None:
@@ -442,8 +467,9 @@
         ALL IMMEDIATE")
         """
         cursor = self.cursor()
+        qn3 = self.ops.qualified_name
         if table_names is None:
-            table_names = self.introspection.get_table_list(cursor)
+            table_names = self.introspection.get_visible_tables_list(cursor)
         for table_name in table_names:
             primary_key_column_name = self.introspection.get_primary_key_column(cursor, table_name)
             if not primary_key_column_name:
@@ -451,15 +477,17 @@
             key_columns = self.introspection.get_key_columns(cursor, table_name)
             for column_name, referenced_table_name, referenced_column_name in key_columns:
                 cursor.execute("""
-                    SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING
-                    LEFT JOIN `%s` as REFERRED
+                    SELECT REFERRING.`%s`, REFERRING.`%s` FROM %s as REFERRING
+                    LEFT JOIN %s as REFERRED
                     ON (REFERRING.`%s` = REFERRED.`%s`)
                     WHERE REFERRING.`%s` IS NOT NULL AND REFERRED.`%s` IS NULL"""
-                    % (primary_key_column_name, column_name, table_name, referenced_table_name,
-                    column_name, referenced_column_name, column_name, referenced_column_name))
+                    % (primary_key_column_name, column_name, qn3(table_name),
+                       qn3(referenced_table_name), column_name,
+                       referenced_column_name, column_name,
+                       referenced_column_name))
                 for bad_row in cursor.fetchall():
                     raise utils.IntegrityError("The row in table '%s' with primary key '%s' has an invalid "
                         "foreign key: %s.%s contains a value '%s' that does not have a corresponding value in %s.%s."
-                        % (table_name, bad_row[0],
-                        table_name, column_name, bad_row[1],
-                        referenced_table_name, referenced_column_name))
+                        % (table_name[1], bad_row[0],
+                        table_name[1], column_name, bad_row[1],
+                        referenced_table_name[1], referenced_column_name))
Index: django/db/backends/oracle/base.py
===================================================================
--- django/db/backends/oracle/base.py	(revision 17914)
+++ django/db/backends/oracle/base.py	(working copy)
@@ -4,12 +4,12 @@
 Requires cx_Oracle: http://cx-oracle.sourceforge.net/
 """
 
-
 import datetime
 import decimal
 import sys
 import warnings
 
+from django.db import QName
 
 def _setup_environment(environ):
     import platform
@@ -48,6 +48,7 @@
 from django.conf import settings
 from django.db import utils
 from django.db.backends import *
+from django.db.backends.util import truncate_name
 from django.db.backends.signals import connection_created
 from django.db.backends.oracle.client import DatabaseClient
 from django.db.backends.oracle.creation import DatabaseCreation
@@ -87,34 +88,42 @@
 class DatabaseOperations(BaseDatabaseOperations):
     compiler_module = "django.db.backends.oracle.compiler"
 
-    def autoinc_sql(self, table, column):
+    def autoinc_sql(self, qname, column):
         # To simulate auto-incrementing primary keys in Oracle, we have to
         # create a sequence and a trigger.
-        sq_name = self._get_sequence_name(table)
-        tr_name = self._get_trigger_name(table)
-        tbl_name = self.quote_name(table)
-        col_name = self.quote_name(column)
+        seq_name = self._get_sequence_name(qname)
+        schema = seq_name.schema.upper()
+        sname = self.connection.ops.qualified_name(seq_name).upper()
+        params = {
+            'sq_name': seq_name.table,
+            'schema': schema,
+            'qualified_sq_name': sname,
+            'tr_name': self._get_trigger_name(qname),
+            'tbl_name': self.qualified_name(qname),
+            'col_name' : self.quote_name(column),
+        }
         sequence_sql = """
 DECLARE
     i INTEGER;
 BEGIN
-    SELECT COUNT(*) INTO i FROM USER_CATALOG
-        WHERE TABLE_NAME = '%(sq_name)s' AND TABLE_TYPE = 'SEQUENCE';
+    SELECT COUNT(*) INTO i FROM ALL_CATALOG
+        WHERE TABLE_NAME = '%(sq_name)s' AND OWNER = '%(schema)s'
+              AND TABLE_TYPE = 'SEQUENCE';
     IF i = 0 THEN
-        EXECUTE IMMEDIATE 'CREATE SEQUENCE "%(sq_name)s"';
+        EXECUTE IMMEDIATE 'CREATE SEQUENCE %(qualified_sq_name)s';
     END IF;
 END;
-/""" % locals()
+/""" % params
         trigger_sql = """
-CREATE OR REPLACE TRIGGER "%(tr_name)s"
+CREATE OR REPLACE TRIGGER %(tr_name)s
 BEFORE INSERT ON %(tbl_name)s
 FOR EACH ROW
 WHEN (new.%(col_name)s IS NULL)
     BEGIN
-        SELECT "%(sq_name)s".nextval
+        SELECT %(qualified_sq_name)s.nextval
         INTO :new.%(col_name)s FROM dual;
     END;
-/""" % locals()
+/""" % params
         return sequence_sql, trigger_sql
 
     def date_extract_sql(self, lookup_type, field_name):
@@ -196,8 +205,10 @@
     def deferrable_sql(self):
         return " DEFERRABLE INITIALLY DEFERRED"
 
-    def drop_sequence_sql(self, table):
-        return "DROP SEQUENCE %s;" % self.quote_name(self._get_sequence_name(table))
+    def drop_sequence_sql(self, qname):
+        seq_name = self._get_sequence_name(qname)
+        qname = self.connection.ops.qualified_name(seq_name)
+        return "DROP SEQUENCE %s;" % qname
 
     def fetch_returned_insert_id(self, cursor):
         return long(cursor._insert_id_var.getvalue())
@@ -213,9 +224,9 @@
         # The DB API definition does not define this attribute.
         return cursor.statement
 
-    def last_insert_id(self, cursor, table_name, pk_name):
-        sq_name = self._get_sequence_name(table_name)
-        cursor.execute('SELECT "%s".currval FROM dual' % sq_name)
+    def last_insert_id(self, cursor, qualified_name, pk_name):
+        sq_name = self._get_sequence_name(qualified_name)
+        cursor.execute('SELECT %s.currval FROM dual' % sq_name)
         return cursor.fetchone()[0]
 
     def lookup_cast(self, lookup_type):
@@ -247,9 +258,23 @@
                                                self.max_name_length())
         return name.upper()
 
+    def qualified_name(self, qname):
+        assert isinstance(qname, QName)
+        schema = qname.schema
+        if not qname.db_format:
+            if not schema:
+                schema = self.connection.schema
+            schema = self.connection.convert_schema(schema)
+        if schema:
+            return "%s.%s" % (self.quote_name(schema),
+                              self.quote_name(qname.table))
+        else:
+            return self.quote_name(qname.table)
+
     def random_function_sql(self):
         return "DBMS_RANDOM.RANDOM"
 
+
     def regex_lookup_9(self, lookup_type):
         raise NotImplementedError("Regexes are not supported in Oracle before version 10g.")
 
@@ -284,46 +309,46 @@
             sql = ['%s %s %s;' % \
                     (style.SQL_KEYWORD('DELETE'),
                      style.SQL_KEYWORD('FROM'),
-                     style.SQL_FIELD(self.quote_name(table)))
+                     style.SQL_FIELD(self.qualified_name(table)))
                     for table in tables]
             # Since we've just deleted all the rows, running our sequence
             # ALTER code will reset the sequence to 0.
             for sequence_info in sequences:
-                sequence_name = self._get_sequence_name(sequence_info['table'])
-                table_name = self.quote_name(sequence_info['table'])
-                column_name = self.quote_name(sequence_info['column'] or 'id')
-                query = _get_sequence_reset_sql() % {'sequence': sequence_name,
-                                                     'table': table_name,
-                                                     'column': column_name}
-                sql.append(query)
+                q = self._sequence_reset_sql_for_col(
+                    sequence_info['qname'], sequence_info['column'] or 'id')
+                sql.append(q)
             return sql
         else:
             return []
 
+    def _sequence_reset_sql_for_col(self, qname, column):
+        qname = self.connection.introspection.qname_converter(qname)
+        qn = self.connection.ops.qualified_name
+        table = qn(qname)
+        sequence_name = self._get_sequence_name(qname)
+        column_name = self.quote_name(column)
+        params = {'sequence': sequence_name.table, 'table': table,
+                  'schema': sequence_name.schema.upper(), 'column': column_name}
+        query = _get_sequence_reset_sql(bool(params['schema']))
+        return query % params
+
     def sequence_reset_sql(self, style, model_list):
         from django.db import models
         output = []
-        query = _get_sequence_reset_sql()
         for model in model_list:
             for f in model._meta.local_fields:
                 if isinstance(f, models.AutoField):
-                    table_name = self.quote_name(model._meta.db_table)
-                    sequence_name = self._get_sequence_name(model._meta.db_table)
-                    column_name = self.quote_name(f.column)
-                    output.append(query % {'sequence': sequence_name,
-                                           'table': table_name,
-                                           'column': column_name})
+                    q = self._sequence_reset_sql_for_col(
+                        model._meta.qualified_name, f.column)
+                    output.append(q)
                     # Only one AutoField is allowed per model, so don't
                     # continue to loop
                     break
             for f in model._meta.many_to_many:
                 if not f.rel.through:
-                    table_name = self.quote_name(f.m2m_db_table())
-                    sequence_name = self._get_sequence_name(f.m2m_db_table())
-                    column_name = self.quote_name('id')
-                    output.append(query % {'sequence': sequence_name,
-                                           'table': table_name,
-                                           'column': column_name})
+                    q = self._sequence_reset_sql_for_col(
+                        f.m2m_qualified_name(), 'id')
+                    output.append(q)
         return output
 
     def start_transaction_sql(self):
@@ -377,13 +402,23 @@
             raise NotImplementedError("Bit-wise or is not supported in Oracle.")
         return super(DatabaseOperations, self).combine_expression(connector, sub_expressions)
 
-    def _get_sequence_name(self, table):
+    def _get_sequence_name(self, qname):
+        assert isinstance(qname, QName)
+        qname = self.connection.introspection.qname_converter(qname,
+                                                              force_schema=True)
         name_length = self.max_name_length() - 3
-        return '%s_SQ' % util.truncate_name(table, name_length).upper()
+        seq_name = '%s_SQ' % util.truncate_name(qname.table,
+                                                name_length).upper()
+        return QName(qname.schema, seq_name, True)
+        #return '%s_SQ' % util.truncate_name(name, name_length).upper()
 
-    def _get_trigger_name(self, table):
+    def _get_trigger_name(self, qname):
+        assert isinstance(qname, QName)
+        qname = self.connection.introspection.qname_converter(qname)
         name_length = self.max_name_length() - 3
-        return '%s_TR' % util.truncate_name(table, name_length).upper()
+        trig_name = '%s_TR' % util.truncate_name(qname.table,
+                                                 name_length).upper()
+        return self.qualified_name(QName(qname.schema, trig_name, True))
 
     def bulk_insert_sql(self, fields, num_values):
         items_sql = "SELECT %s FROM DUAL" % ", ".join(["%s"] * len(fields))
@@ -444,6 +479,13 @@
         self.creation = DatabaseCreation(self)
         self.introspection = DatabaseIntrospection(self)
         self.validation = BaseDatabaseValidation(self)
+    
+    def convert_schema(self, schema):
+        schema = schema or self.schema
+        if schema and self.test_schema_prefix:
+            return truncate_name('%s%s' % (self.test_schema_prefix, schema),
+                                 self.ops.max_name_length())
+        return schema
 
     def check_constraints(self, table_names=None):
         """
@@ -476,7 +518,11 @@
             conn_params = self.settings_dict['OPTIONS'].copy()
             if 'use_returning_into' in conn_params:
                 del conn_params['use_returning_into']
-            self.connection = Database.connect(conn_string, **conn_params)
+            try:
+                self.connection = Database.connect(conn_string, **conn_params)
+            except:
+                print conn_string
+                raise
             cursor = FormatStylePlaceholderCursor(self.connection)
             # Set oracle date to ansi date format.  This only needs to execute
             # once when we create a new connection. We also set the Territory
@@ -814,14 +860,29 @@
     return s
 
 
-def _get_sequence_reset_sql():
+def _get_sequence_reset_sql(with_schema=False):
     # TODO: colorize this SQL code with style.SQL_KEYWORD(), etc.
-    return """
+    if with_schema:
+        return """
 DECLARE
     table_value integer;
     seq_value integer;
 BEGIN
     SELECT NVL(MAX(%(column)s), 0) INTO table_value FROM %(table)s;
+    SELECT NVL(last_number - cache_size, 0) INTO seq_value FROM all_sequences
+           WHERE sequence_name = '%(sequence)s' AND sequence_owner = '%(schema)s';
+    WHILE table_value > seq_value LOOP
+        SELECT "%(schema)s"."%(sequence)s".nextval INTO seq_value FROM dual;
+    END LOOP;
+END;
+/"""
+    else:
+        return """
+DECLARE
+    table_value integer;
+    seq_value integer;
+BEGIN
+    SELECT NVL(MAX(%(column)s), 0) INTO table_value FROM %(table)s;
     SELECT NVL(last_number - cache_size, 0) INTO seq_value FROM user_sequences
            WHERE sequence_name = '%(sequence)s';
     WHILE table_value > seq_value LOOP
Index: django/db/backends/oracle/introspection.py
===================================================================
--- django/db/backends/oracle/introspection.py	(revision 17914)
+++ django/db/backends/oracle/introspection.py	(working copy)
@@ -1,3 +1,4 @@
+from django.db import QName
 from django.db.backends import BaseDatabaseIntrospection
 import cx_Oracle
 import re
@@ -37,56 +38,85 @@
             return super(DatabaseIntrospection, self).get_field_type(
                 data_type, description)
 
-    def get_table_list(self, cursor):
-        "Returns a list of table names in the current database."
-        cursor.execute("SELECT TABLE_NAME FROM USER_TABLES")
-        return [row[0].lower() for row in cursor.fetchall()]
+    def get_visible_tables_list(self, cursor):
+        "Returns a list of visible tables"
+        return self.get_qualified_tables_list(cursor, [self.connection.settings_dict['USER']])
 
-    def get_table_description(self, cursor, table_name):
+    def get_qualified_tables_list(self, cursor, schemas):
+        "Returns a list of table names in the given schemas list."
+        default_schema = self.connection.convert_schema(None)
+        if default_schema:
+            schemas.append(default_schema)
+        if not schemas:
+            return []
+        param_list = ', '.join(['%s']*len(schemas))
+        schemas = [s.upper() for s in schemas]
+        cursor.execute("""
+            SELECT OWNER, TABLE_NAME
+              FROM ALL_TABLES WHERE OWNER in (%s)""" % param_list, schemas)
+        return [QName(row[0].lower(), row[1].lower(), True)
+                for row in cursor.fetchall()]
+
+    def get_table_description(self, cursor, qname):
         "Returns a description of the table, with the DB-API cursor.description interface."
-        cursor.execute("SELECT * FROM %s WHERE ROWNUM < 2" % self.connection.ops.quote_name(table_name))
+        cursor.execute("SELECT * FROM %s WHERE ROWNUM < 2"
+                       % self.connection.ops.qualified_name(qname))
         description = []
         for desc in cursor.description:
             description.append((desc[0].lower(),) + desc[1:])
         return description
 
-    def table_name_converter(self, name):
-        "Table name comparison is case insensitive under Oracle"
+    def identifier_converter(self, name):
         return name.lower()
 
-    def _name_to_index(self, cursor, table_name):
+    def qname_converter(self, qname, force_schema=False):
+        assert isinstance(qname, QName)
+        if qname.db_format and (qname.schema or not force_schema):
+            return qname
+        schema = self.connection.convert_schema(qname.schema)
+        if not schema and force_schema:
+            schema = self.connection.settings_dict['USER']
+        return QName(schema, qname.table, True)
+
+    def _name_to_index(self, cursor, qname):
         """
         Returns a dictionary of {field_name: field_index} for the given table.
         Indexes are 0-based.
         """
-        return dict([(d[0], i) for i, d in enumerate(self.get_table_description(cursor, table_name))])
+        return dict([(d[0], i) for i, d in enumerate(self.get_table_description(cursor, qname))])
 
-    def get_relations(self, cursor, table_name):
+    def get_relations(self, cursor, qname):
         """
         Returns a dictionary of {field_index: (field_index_other_table, other_table)}
         representing all relationships to the given table. Indexes are 0-based.
         """
-        table_name = table_name.upper()
+        qname = self.qname_converter(qname, force_schema=True)
+        schema, table = qname.schema.upper(), qname.table.upper()
         cursor.execute("""
-    SELECT ta.column_id - 1, tb.table_name, tb.column_id - 1
-    FROM   user_constraints, USER_CONS_COLUMNS ca, USER_CONS_COLUMNS cb,
-           user_tab_cols ta, user_tab_cols tb
-    WHERE  user_constraints.table_name = %s AND
+    SELECT ta.column_id - 1, tb.table_name, tb.owner, tb.column_id - 1
+    FROM   all_constraints, ALL_CONS_COLUMNS ca, ALL_CONS_COLUMNS cb,
+           all_tab_cols ta, all_tab_cols tb
+    WHERE  all_constraints.table_name = %s AND
+           all_constraints.owner = %s AND
            ta.table_name = %s AND
+           ta.owner = %s AND
            ta.column_name = ca.column_name AND
            ca.table_name = %s AND
-           user_constraints.constraint_name = ca.constraint_name AND
-           user_constraints.r_constraint_name = cb.constraint_name AND
+           ca.owner = %s AND
+           all_constraints.constraint_name = ca.constraint_name AND
+           all_constraints.r_constraint_name = cb.constraint_name AND
            cb.table_name = tb.table_name AND
            cb.column_name = tb.column_name AND
-           ca.position = cb.position""", [table_name, table_name, table_name])
+           ca.position = cb.position""", [table, schema, table, schema,
+                                          table, schema])
 
         relations = {}
         for row in cursor.fetchall():
-            relations[row[0]] = (row[2], row[1].lower())
+            relations[row[0]] = (
+                row[3], QName(row[2].lower(), row[1].lower(), True))
         return relations
 
-    def get_indexes(self, cursor, table_name):
+    def get_indexes(self, cursor, qname):
         """
         Returns a dictionary of fieldname -> infodict for the given table,
         where each infodict is in the format:
@@ -96,27 +126,45 @@
         # This query retrieves each index on the given table, including the
         # first associated field name
         # "We were in the nick of time; you were in great peril!"
+        qname = self.qname_converter(qname, force_schema=True)
+        schema, table = qname.schema.upper(), qname.table.upper()
+        # There can be multiple constraints for a given column, and we 
+        # are interested if _any_ of them is unique or primary key, hence
+        # the group by + max.
         sql = """\
-SELECT LOWER(all_tab_cols.column_name) AS column_name,
-       CASE user_constraints.constraint_type
-           WHEN 'P' THEN 1 ELSE 0
-       END AS is_primary_key,
-       CASE user_indexes.uniqueness
-           WHEN 'UNIQUE' THEN 1 ELSE 0
-       END AS is_unique
-FROM   all_tab_cols, user_cons_columns, user_constraints, user_ind_columns, user_indexes
-WHERE  all_tab_cols.column_name = user_cons_columns.column_name (+)
-  AND  all_tab_cols.table_name = user_cons_columns.table_name (+)
-  AND  user_cons_columns.constraint_name = user_constraints.constraint_name (+)
-  AND  user_constraints.constraint_type (+) = 'P'
-  AND  user_ind_columns.column_name (+) = all_tab_cols.column_name
-  AND  user_ind_columns.table_name (+) = all_tab_cols.table_name
-  AND  user_indexes.uniqueness (+) = 'UNIQUE'
-  AND  user_indexes.index_name (+) = user_ind_columns.index_name
-  AND  all_tab_cols.table_name = UPPER(%s)
+SELECT column_name, max(is_primary_key), max(is_unique)
+  FROM (
+    SELECT LOWER(all_tab_cols.column_name) AS column_name,
+           CASE all_constraints.constraint_type
+               WHEN 'P' THEN 1 ELSE 0
+           END AS is_primary_key,
+           CASE all_indexes.uniqueness
+               WHEN 'UNIQUE' THEN 1 ELSE 0
+           END AS is_unique
+    FROM   all_tab_cols, all_cons_columns, all_constraints, all_ind_columns, all_indexes
+    WHERE  all_tab_cols.column_name = all_cons_columns.column_name (+)
+      AND  all_tab_cols.table_name = all_cons_columns.table_name (+)
+      AND  all_tab_cols.owner = all_cons_columns.owner (+)
+      AND  all_cons_columns.constraint_name = all_constraints.constraint_name (+)
+      AND  all_cons_columns.owner = all_constraints.owner (+)
+      AND  all_constraints.constraint_type (+) = 'P'
+      AND  all_ind_columns.column_name (+) = all_tab_cols.column_name
+      AND  all_ind_columns.table_name (+) = all_tab_cols.table_name
+      AND  all_ind_columns.index_owner (+) = all_tab_cols.owner
+      AND  all_indexes.uniqueness (+) = 'UNIQUE'
+      AND  all_indexes.index_name (+) = all_ind_columns.index_name
+      AND  all_indexes.table_owner (+) = all_ind_columns.index_owner
+      AND  all_tab_cols.table_name = %s
+      AND  all_tab_cols.owner = %s
+    )
+GROUP BY column_name
 """
-        cursor.execute(sql, [table_name])
+        cursor.execute(sql, [table, schema])
         indexes = {}
         for row in cursor.fetchall():
             indexes[row[0]] = {'primary_key': row[1], 'unique': row[2]}
         return indexes
+
+    def get_schema_list(self, cursor):
+        cursor.execute("SELECT USERNAME FROM ALL_USERS")
+        return [r[0] for r in cursor.fetchall()]
Index: django/db/backends/oracle/creation.py
===================================================================
--- django/db/backends/oracle/creation.py	(revision 17914)
+++ django/db/backends/oracle/creation.py	(working copy)
@@ -1,6 +1,8 @@
 import sys
 import time
+from django.db import QName
 from django.db.backends.creation import BaseDatabaseCreation
+from django.core.management.color import no_style
 
 TEST_DATABASE_PREFIX = 'test_'
 PASSWORD = 'Im_a_lumberjack'
@@ -43,14 +45,13 @@
     def __init__(self, connection):
         super(DatabaseCreation, self).__init__(connection)
 
-    def _create_test_db(self, verbosity=1, autoclobber=False):
+    def _get_ddl_parameters(self):
         TEST_NAME = self._test_database_name()
         TEST_USER = self._test_database_user()
         TEST_PASSWD = self._test_database_passwd()
         TEST_TBLSPACE = self._test_database_tblspace()
         TEST_TBLSPACE_TMP = self._test_database_tblspace_tmp()
-
-        parameters = {
+        return {
             'dbname': TEST_NAME,
             'user': TEST_USER,
             'password': TEST_PASSWD,
@@ -58,6 +59,8 @@
             'tblspace_temp': TEST_TBLSPACE_TMP,
         }
 
+    def _create_test_db(self, verbosity=1, autoclobber=False, schemas=[]):
+        parameters = self._get_ddl_parameters()
         cursor = self.connection.cursor()
         if self._test_database_create():
             try:
@@ -65,7 +68,9 @@
             except Exception, e:
                 sys.stderr.write("Got an error creating the test database: %s\n" % e)
                 if not autoclobber:
-                    confirm = raw_input("It appears the test database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % TEST_NAME)
+                    confirm = raw_input("It appears the test database, %(dbname)s, already "
+                                        "exists. Type 'yes' to delete it, or 'no' to cancel: "
+                                        % parameters)
                 if autoclobber or confirm == 'yes':
                     try:
                         if verbosity >= 1:
@@ -83,11 +88,13 @@
             if verbosity >= 1:
                 print "Creating test user..."
             try:
-                self._create_test_user(cursor, parameters, verbosity)
+                self._create_test_user(cursor, parameters, verbosity, dba=bool(schemas))
             except Exception, e:
                 sys.stderr.write("Got an error creating the test user: %s\n" % e)
                 if not autoclobber:
-                    confirm = raw_input("It appears the test user, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % TEST_USER)
+                    confirm = raw_input("It appears the test user, %(user)s, already exists. "
+                                        "Type 'yes' to delete it, or 'no' to cancel: "
+                                        % parameters)
                 if autoclobber or confirm == 'yes':
                     try:
                         if verbosity >= 1:
@@ -95,7 +102,7 @@
                         self._destroy_test_user(cursor, parameters, verbosity)
                         if verbosity >= 1:
                             print "Creating test user..."
-                        self._create_test_user(cursor, parameters, verbosity)
+                        self._create_test_user(cursor, parameters, verbosity, dba=bool(schemas))
                     except Exception, e:
                         sys.stderr.write("Got an error recreating the test user: %s\n" % e)
                         sys.exit(2)
@@ -105,8 +112,8 @@
 
         self.connection.settings_dict['SAVED_USER'] = self.connection.settings_dict['USER']
         self.connection.settings_dict['SAVED_PASSWORD'] = self.connection.settings_dict['PASSWORD']
-        self.connection.settings_dict['TEST_USER'] = self.connection.settings_dict['USER'] = TEST_USER
-        self.connection.settings_dict['PASSWORD'] = TEST_PASSWD
+        self.connection.settings_dict['TEST_USER'] = self.connection.settings_dict['USER'] = parameters['user']
+        self.connection.settings_dict['PASSWORD'] = parameters['password']
 
         return self.connection.settings_dict['NAME']
 
@@ -115,33 +122,21 @@
         Destroy a test database, prompting the user for confirmation if the
         database already exists. Returns the name of the test database created.
         """
-        TEST_NAME = self._test_database_name()
-        TEST_USER = self._test_database_user()
-        TEST_PASSWD = self._test_database_passwd()
-        TEST_TBLSPACE = self._test_database_tblspace()
-        TEST_TBLSPACE_TMP = self._test_database_tblspace_tmp()
+        parameters = self._get_ddl_parameters()
 
         self.connection.settings_dict['USER'] = self.connection.settings_dict['SAVED_USER']
         self.connection.settings_dict['PASSWORD'] = self.connection.settings_dict['SAVED_PASSWORD']
 
-        parameters = {
-            'dbname': TEST_NAME,
-            'user': TEST_USER,
-            'password': TEST_PASSWD,
-            'tblspace': TEST_TBLSPACE,
-            'tblspace_temp': TEST_TBLSPACE_TMP,
-        }
-
         cursor = self.connection.cursor()
         time.sleep(1) # To avoid "database is being accessed by other users" errors.
+        if self._test_database_create():
+            if verbosity >= 1:
+                print 'Destroying test database tables...'
+            self._execute_test_db_destruction(cursor, parameters, verbosity)
         if self._test_user_create():
             if verbosity >= 1:
                 print 'Destroying test user...'
             self._destroy_test_user(cursor, parameters, verbosity)
-        if self._test_database_create():
-            if verbosity >= 1:
-                print 'Destroying test database tables...'
-            self._execute_test_db_destruction(cursor, parameters, verbosity)
         self.connection.close()
 
     def _execute_test_db_creation(self, cursor, parameters, verbosity):
@@ -159,16 +154,19 @@
         ]
         self._execute_statements(cursor, statements, parameters, verbosity)
 
-    def _create_test_user(self, cursor, parameters, verbosity):
+    def _create_test_user(self, cursor, parameters, verbosity, dba=False):
         if verbosity >= 2:
             print "_create_test_user(): username = %s" % parameters['user']
+        parameters = parameters.copy()
+        parameters['dba'] = ', DBA' if dba else ''
+
         statements = [
             """CREATE USER %(user)s
                IDENTIFIED BY %(password)s
                DEFAULT TABLESPACE %(tblspace)s
                TEMPORARY TABLESPACE %(tblspace_temp)s
             """,
-            """GRANT CONNECT, RESOURCE TO %(user)s""",
+            """GRANT CONNECT, RESOURCE %(dba)s TO %(user)s""",
         ]
         self._execute_statements(cursor, statements, parameters, verbosity)
 
@@ -186,10 +184,14 @@
             print "_destroy_test_user(): user=%s" % parameters['user']
             print "Be patient.  This can take some time..."
         statements = [
-            'DROP USER %(user)s CASCADE',
+            self.sql_destroy_schema(parameters['user'], style=None)
         ]
         self._execute_statements(cursor, statements, parameters, verbosity)
 
+    def sql_destroy_schema(self, schema, style):
+        return "DROP USER %s CASCADE" % schema
+
+
     def _execute_statements(self, cursor, statements, parameters, verbosity):
         for template in statements:
             stmt = template % parameters
@@ -272,3 +274,175 @@
 
     def set_autocommit(self):
         self.connection.connection.autocommit = True
+
+    def _create_test_schemas(self, verbosity, schemas, autoclobber):
+        if not self._test_user_create():
+            return []
+        cursor = self.connection.cursor()
+        parameters = self._get_ddl_parameters()
+        parameters['authorization'] = parameters['user']
+        conv = self.connection.introspection.identifier_converter
+        existing_schemas = [conv(s) for s in self.connection.introspection.get_schema_list(cursor)]
+        conflicts = [conv(s) for s in existing_schemas if conv(s) in schemas]
+        if conflicts:
+            print 'The following users already exists: %s' % ', '.join(conflicts) 
+            if not autoclobber:
+                confirm = raw_input(
+                    "Type 'yes' if you would like to try deleting these users "
+                    "or 'no' to cancel: ")
+            if autoclobber or confirm == 'yes':
+                for schema in conflicts:
+                    parameters['user'] = schema
+                    if verbosity >= 1:
+                        print "Destroying user %s" % schema
+                    self._destroy_test_user(cursor, parameters, verbosity)
+                    existing_schemas.remove(schema)
+            else:
+                print "Tests cancelled."
+                sys.exit(1)
+           
+        to_create = [s for s in schemas if s not in existing_schemas]
+        for schema in to_create:
+            parameters['user'] = schema
+            if verbosity >= 1:
+                print "Creating user %s" % schema
+            self._create_test_user(cursor, parameters, verbosity)
+        return to_create
+
+    def needs_separate_conn(self, from_qname, to_qname):
+        conv = self.connection.introspection.qname_converter
+        def_schema = conv(QName(None, None, False), force_schema=True).schema
+        from_qname = conv(from_qname, force_schema=True)
+        to_qname = conv(to_qname, force_schema=True)
+        return (def_schema != from_qname.schema
+                or to_qname.schema != from_qname.schema)
+
+    def sql_for_inline_foreign_key_references(self, field, known_models, style):
+        """
+        Return the SQL snippet defining the foreign key reference for a field.
+
+        Oracle doesn't let you do cross-schema foreign keys, except if you
+        are connected to the "from" schema. Don't ask why.
+        """
+        if self.needs_separate_conn(field.model._meta.qualified_name,
+                                    field.rel.to._meta.qualified_name):
+            return [], True
+        return super(DatabaseCreation, self).sql_for_inline_foreign_key_references(field, known_models, style)
+
+    def sql_for_pending_references(self, model, style, pending_references,
+                                   second_pass=False):
+        """
+        Sad fact of life: On Oracle it is impossible to do cross-schema
+        references unless you explisitly grant REFERENCES on the referenced
+        table, and in addition the reference is made from the schema
+        containing the altered table (the one getting the new constraint).
+        To make this even nicer, it is impossible to do the GRANT using the
+        same user we are giving the REFERENCES right, as you can't GRANT
+        yourself.
+
+        The solution we are using is to do the pending cross-schema references
+        in two stages after all tables have been created:
+            1) Connect as the foreign key's target table owner, and grant
+               REFERENCES to all users needing to do foreign keys.
+            2) Connect as the source table's owner, and create the foreign
+               keys.
+        To support this arrangement, we will create only non-cross-schema
+        references unless we are explicitly told by the second_pass flag
+        that it is safe to do the cross schema references.
+
+        It is possible to grant REFERENCES to public (but it seems other roles
+        will not work), but as we need to anyways do this multi-connection
+        dance it seems better to do the grants explicitly only when needed.
+        """
+        if second_pass:
+            return super(DatabaseCreation, self).sql_for_pending_references(
+                model, style, pending_references)
+        # Split the "safe" and "unsafe" references apart, and call
+        # the super() method for the safe set.
+        cross_schema_refs = []
+        single_schema_refs = []
+        if model in pending_references:
+            for rel_class, f in pending_references[model]:
+                if self.needs_separate_conn(rel_class._meta.qualified_name,
+                                            model._meta.qualified_name):
+                    cross_schema_refs.append((rel_class, f))
+                else:
+                    single_schema_refs.append((rel_class, f))
+        sql = []
+        if single_schema_refs:
+            pending_references[model] = single_schema_refs
+            sql = super(DatabaseCreation, self).sql_for_pending_references(
+                model, style, pending_references)
+        if cross_schema_refs:
+            pending_references[model] = cross_schema_refs
+        return sql
+
+    def post_create_pending_references(self, pending_references, as_sql=False):
+        # Build a dictionary: from_schema -> [(model, refs)...]
+        references_to_schema = {}
+        sql = []
+        conv = self.connection.introspection.qname_converter
+        for model, refs in pending_references.items():
+            schema = conv(model._meta.qualified_name, force_schema=True).schema
+            if schema not in references_to_schema:
+                references_to_schema[schema] = []
+            references_to_schema[schema].append((model, refs))
+        # Pass 1: give grants.
+        for schema, all_refs in references_to_schema.items():
+            grant_to = set()
+            for model, refs in all_refs:
+                for ref in refs:
+                    to_user = conv(ref[0]._meta.qualified_name,
+                                   force_schema=True).schema
+                    if to_user != schema:
+                        grant_to.add((model, to_user))
+            sql.extend(self._grant_references(schema, grant_to, as_sql))
+        # Prepare for pass 2. This time we must connect as the user
+        # of the altered table's schema. So, first build a dictionary of
+        # from_schema -> [{model: [refs]}], that is, build a
+        # pending_references for each schema separately.
+        references_from_schema = {}
+        for model, refs in pending_references.items():
+            for ref in refs:
+                schema = conv(ref[0]._meta.qualified_name,
+                              force_schema=True).schema
+                if schema not in references_from_schema:
+                    references_from_schema[schema] = {}
+                if model not in references_from_schema[schema]:
+                    references_from_schema[schema][model] = []
+                references_from_schema[schema][model].append(ref)
+        # Pass 2: create the actual references
+        for schema, ref_dict in references_from_schema.items():
+            per_schema_sql = ['-- Connect as user "%s"' % schema] if as_sql else []
+            for model, refs in ref_dict.items():
+                ref_sql = self.sql_for_pending_references(model, no_style(),
+                                                          ref_dict, second_pass=True)
+                per_schema_sql.extend(ref_sql)
+            if not as_sql:
+                self._run_sql_as_user(schema, per_schema_sql)
+            sql.extend(per_schema_sql)
+        return sql
+
+    def _grant_references(self, schema, grant_to, as_sql):
+        sql = ['-- Connect as user "%s"' % schema] if as_sql else []
+        qn = self.connection.ops.quote_name
+        for model, user in grant_to:
+            sql.append('GRANT REFERENCES ON %s TO %s'
+                       % (qn(model._meta.db_table), qn(user)))
+        if not as_sql:
+            self._run_sql_as_user(schema, sql)
+        return sql
+
+    def _run_sql_as_user(self, user, sql):
+        if not sql:
+            return
+        self.connection.close()
+        try:
+            old_settings = self.connection.settings_dict.copy()
+            self.connection.settings_dict['USER'] = user
+            cursor = self.connection.cursor()
+            for q in sql:
+                cursor.execute(q)
+        finally:
+            self.connection.close()
+            self.connection.settings_dict = old_settings
Index: django/db/backends/__init__.py
===================================================================
--- django/db/backends/__init__.py	(revision 17914)
+++ django/db/backends/__init__.py	(working copy)
@@ -7,7 +7,7 @@
 from contextlib import contextmanager
 
 from django.conf import settings
-from django.db import DEFAULT_DB_ALIAS
+from django.db import DEFAULT_DB_ALIAS, QName
 from django.db.backends import util
 from django.db.transaction import TransactionManagementError
 from django.utils.importlib import import_module
@@ -44,7 +44,18 @@
 
     def __ne__(self, other):
         return not self == other
+    
+    def _get_schema(self):
+        return self.settings_dict['SCHEMA']
+    schema = property(_get_schema)
 
+    def _get_test_schema_prefix(self):
+        return self.settings_dict['TEST_SCHEMA_PREFIX']
+    test_schema_prefix = property(_get_test_schema_prefix)
+
+    def convert_schema(self, schema):
+        return schema or self.schema
+
     def _commit(self):
         if self.connection is not None:
             return self.connection.commit()
@@ -311,6 +322,13 @@
     def make_debug_cursor(self, cursor):
         return util.CursorDebugWrapper(cursor, self)
 
+    def qname(self, model):
+        """
+        Given a model class or instance, returns its current database table
+        name in schema qualified format.
+        """
+        return self.ops.qualified_name(model._meta.qualified_name)
+
 class BaseDatabaseFeatures(object):
     allows_group_by_pk = False
     # True if django.db.backend.utils.typecast_timestamp is used on values
@@ -404,11 +422,17 @@
     _confirmed = False
     supports_transactions = None
     supports_stddev = None
+    supports_foreign_keys = True
     can_introspect_foreign_keys = None
 
     # Support for the DISTINCT ON clause
     can_distinct_on_fields = False
 
+    # If the database has databases and schemas as different concepts
+    # or plain fakes schemas, it is safe to skip conflicts checking in
+    # testing on that database.
+    namespaced_schemas = False
+
     def __init__(self, connection):
         self.connection = connection
 
@@ -461,7 +485,7 @@
         self.connection = connection
         self._cache = None
 
-    def autoinc_sql(self, table, column):
+    def autoinc_sql(self, qualified_name, column):
         """
         Returns any SQL needed to support auto-incrementing primary keys, or
         None if no SQL is necessary.
@@ -513,7 +537,7 @@
         """
         return "DROP CONSTRAINT"
 
-    def drop_sequence_sql(self, table):
+    def drop_sequence_sql(self, qualified_name):
         """
         Returns any SQL necessary to drop the sequence for the given table.
         Returns None if no SQL is necessary.
@@ -594,7 +618,7 @@
 
         return smart_unicode(sql) % u_params
 
-    def last_insert_id(self, cursor, table_name, pk_name):
+    def last_insert_id(self, cursor, qualified_name, pk_name):
         """
         Given a cursor object that has just performed an INSERT statement into
         a table that has an auto-incrementing ID, returns the newly created ID.
@@ -673,6 +697,20 @@
         """
         raise NotImplementedError()
 
+    def qualified_name(self, qualified_name, from_model=False):
+        """
+        Formats the given schema, table_name tuple into database's
+        qualified and quoted name format. The schema can be None.
+
+        We need to know if the name if from an existing database
+        table, or from a model. The reason is that some backends
+        do modifications to the name (schema-prefix the table name)
+        at runtime, and we must not do that repeatedly. Hence, if
+        the name comes from the DB and is already schema-prefixed,
+        then we must not schema-prefix it again.
+        """
+        raise NotImplementedError
+
     def random_function_sql(self):
         """
         Returns a SQL expression that returns a random value.
@@ -718,7 +756,7 @@
         """
         return ''
 
-    def sql_flush(self, style, tables, sequences):
+    def sql_flush(self, style, tables, sequences, from_db):
         """
         Returns a list of SQL statements required to remove all data from
         the given database tables (without actually removing the tables
@@ -726,6 +764,9 @@
 
         The `style` argument is a Style object as returned by either
         color_style() or no_style() in django.core.management.color.
+
+        The from_db argument tells if the names are coming from database
+        or from model._meta, needed for schema support.
         """
         raise NotImplementedError()
 
@@ -883,25 +924,88 @@
         distinguish between a FloatField and IntegerField, for example."""
         return self.data_types_reverse[data_type]
 
-    def table_name_converter(self, name):
-        """Apply a conversion to the name for the purposes of comparison.
+    def qname_converter(self, qname, force_schema=False):
+        """
+        Apply a conversion to the name for the purposes of comparison.
 
         The default table name converter is for case sensitive comparison.
+
+        The given name must be a QName. If force_schema is set, then backends
+        should try to append a default schema name to the given name if
+        applicable for the backend.
         """
-        return name
+        return qname
 
+    def identifier_converter(self, identifier):
+        """
+        On some backends we need to do a little acrobaty to convert the names
+        from the DB and names from Models into consistent format. For example,
+        the return types from DB might be upper-case, but we need them
+        lower-case for comparisons. This method can be used to convert
+        identifiers into consistent format.
+        """
+        return identifier
+
+
     def table_names(self):
-        "Returns a list of names of all tables that exist in the database."
+        "Returns a list of names of all tables that are visible in the database."
         cursor = self.connection.cursor()
-        return self.get_table_list(cursor)
+        return self.get_visible_tables_list(cursor)
 
+    def get_visible_tables_list(self, cursoe):
+        """
+        Returns all visible (in "search path") tables from the database.
+        """
+        return []
+
+    def qualified_names(self, schemas=None):
+        """
+        Returns qualified table names for all schemas Django is using.
+        """
+        cursor = self.connection.cursor()
+        if schemas is None:
+            return self.get_visible_tables_list(cursor)
+        else:
+            return 
+        
+    def all_qualified_names(self):
+        """
+        Gets all table names from the database. Note that it is intentional
+        that visible tables appear both as unqualified and qualified tables.
+        """
+        cursor = self.connection.cursor()
+        nonqualified_tables = self.get_visible_tables_list(cursor)
+        schemas = self.connection.creation.get_schemas()
+        schemas = [self.connection.convert_schema(s) for s in schemas]
+        qualified_tables = self.get_qualified_tables_list(cursor, schemas)
+        return set([QName(None, t, from_db) for _, t, from_db
+                    in nonqualified_tables]
+                   + qualified_tables)
+    
+    def get_qualified_tables_list(self, cursor, schemas):
+        """
+        Returns schema qualified names (as pair schema, tbl_name) of all
+        tables in the given schemas.
+        """
+        return []
+
+    def get_schema_list(self, cursor):
+        "Returns a list of schemas that exist in the database"
+        return []
+    
+
+    def schema_names(self):
+        "Returns a list of schemas that exist in the database"
+        cursor = self.connection.cursor()
+        return self.get_schema_list(cursor)
+
     def django_table_names(self, only_existing=False):
         """
-        Returns a list of all table names that have associated Django models and
-        are in INSTALLED_APPS.
+        Returns a list of all table's qualified names that have associated
+        Django models and are in INSTALLED_APPS.
 
-        If only_existing is True, the resulting list will only include the tables
-        that actually exist in the database.
+        If only_existing is True, the resulting list will only include the
+        tables that actually exist in the database.
         """
         from django.db import models, router
         tables = set()
@@ -911,34 +1015,43 @@
                     continue
                 if not router.allow_syncdb(self.connection.alias, model):
                     continue
-                tables.add(model._meta.db_table)
-                tables.update([f.m2m_db_table() for f in model._meta.local_many_to_many])
+                tables.add(model._meta.qualified_name)
+                tables.update([f.m2m_qualified_name()
+                               for f in model._meta.local_many_to_many])
         tables = list(tables)
         if only_existing:
-            existing_tables = self.table_names()
-            tables = [
-                t
-                for t in tables
-                if self.table_name_converter(t) in existing_tables
-            ]
-        return tables
+            found_tables = []
+            existing_tables = self.all_qualified_names()
+            found_tables.extend([
+                t for t in tables
+                if self.qname_converter(t) in existing_tables
+            ])
+            return found_tables
+        else:
+            return tables
 
     def installed_models(self, tables):
-        "Returns a set of all models represented by the provided list of table names."
+        """
+        Returns a set of all models represented by the provided list of table names.
+
+        The given tables are assumed to be pre-converted.
+        """
         from django.db import models, router
         all_models = []
         for app in models.get_apps():
             for model in models.get_models(app):
                 if router.allow_syncdb(self.connection.alias, model):
                     all_models.append(model)
-        tables = map(self.table_name_converter, tables)
         return set([
             m for m in all_models
-            if self.table_name_converter(m._meta.db_table) in tables
+            if self.qname_converter(m._meta.qualified_name) in tables
         ])
 
     def sequence_list(self):
-        "Returns a list of information about all DB sequences for all models in all apps."
+        """
+        Returns a list of information about all DB sequences for all models
+        in all apps.
+        """
         from django.db import models, router
 
         apps = models.get_apps()
@@ -952,14 +1065,17 @@
                     continue
                 for f in model._meta.local_fields:
                     if isinstance(f, models.AutoField):
-                        sequence_list.append({'table': model._meta.db_table, 'column': f.column})
+                        qname = self.qname_converter(model._meta.qualified_name)
+                        sequence_list.append({'qname': qname, 'column': f.column})
                         break # Only one AutoField is allowed per model, so don't bother continuing.
 
                 for f in model._meta.local_many_to_many:
                     # If this is an m2m using an intermediate table,
                     # we don't need to reset the sequence.
                     if f.rel.through is None:
-                        sequence_list.append({'table': f.m2m_db_table(), 'column': None})
+                        qname = self.qname_converter(f.m2m_qualified_name())
+                        sequence_list.append({'qname': qname,
+                                              'column': None})
 
         return sequence_list
 
Index: django/core/management/commands/syncdb.py
===================================================================
--- django/core/management/commands/syncdb.py	(revision 17914)
+++ django/core/management/commands/syncdb.py	(working copy)
@@ -55,9 +55,14 @@
         db = options.get('database')
         connection = connections[db]
         cursor = connection.cursor()
-
+        converter = connection.introspection.qname_converter
+        # We might fetch the same table multiple times - once as qualified and
+        # once as visible table (None, t). That is wanted, so that we can easily
+        # see if a model with schema = None is installed, as well as if model with
+        # locked schema is installed.
+        tables = connection.introspection.all_qualified_names()
+        
         # Get a list of already installed *models* so that references work right.
-        tables = connection.introspection.table_names()
         seen_models = connection.introspection.installed_models(tables)
         created_models = set()
         pending_references = {}
@@ -71,9 +76,8 @@
         ]
         def model_installed(model):
             opts = model._meta
-            converter = connection.introspection.table_name_converter
-            return not ((converter(opts.db_table) in tables) or
-                (opts.auto_created and converter(opts.auto_created._meta.db_table) in tables))
+            return not ((converter(opts.qualified_name) in tables) or
+                (opts.auto_created and converter(opts.auto_created._meta.qualified_name) in tables))
 
         manifest = SortedDict(
             (app_name, filter(model_installed, model_list))
@@ -83,30 +87,57 @@
         # Create the tables for each model
         if verbosity >= 1:
             print "Creating tables ..."
+        seen_schemas = connection.introspection.schema_names()
+        seen_schemas = set([connection.introspection.identifier_converter(s)
+                            for s in seen_schemas])
+
         for app_name, model_list in manifest.items():
             for model in model_list:
                 # Create the model's database table, if it doesn't already exist.
                 if verbosity >= 3:
                     print "Processing %s.%s model" % (app_name, model._meta.object_name)
-                sql, references = connection.creation.sql_create_model(model, self.style, seen_models)
+                sql = []
+                schema = connection.convert_schema(model._meta.qualified_name[0])
+                if schema and schema not in seen_schemas:
+                    q = connection.creation.sql_create_schema(schema, self.style)
+                    if q:
+                        sql.append(q)
+                    seen_schemas.add(schema)
+                table_sql, references = connection.creation.sql_create_model(model, self.style, seen_models)
+                sql.extend(table_sql)
                 seen_models.add(model)
                 created_models.add(model)
                 for refto, refs in references.items():
                     pending_references.setdefault(refto, []).extend(refs)
                     if refto in seen_models:
-                        sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references))
-                sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references))
+                        ref_sql = connection.creation.sql_for_pending_references(
+                            refto, self.style, pending_references)
+                        if ref_sql:
+                            sql.extend(ref_sql)
+                ref_sql = sql.extend(connection.creation.sql_for_pending_references(
+                    model, self.style, pending_references))
+                if ref_sql:
+                    sql.extend(ref_sql)
                 if verbosity >= 1 and sql:
-                    print "Creating table %s" % model._meta.db_table
+                    if model._meta.db_schema:
+                        print "Creating table %s.%s" % model._meta.qualified_name
+                    else:
+                        print "Creating table %s" % model._meta.db_table
                 for statement in sql:
                     cursor.execute(statement)
-                tables.append(connection.introspection.table_name_converter(model._meta.db_table))
+                tables.add(connection.introspection.qname_converter(model._meta.qualified_name))
 
-
         transaction.commit_unless_managed(using=db)
+        # We need to see if there are still some pending references left: this
+        # is possible on backends where we must do cross-schema references
+        # using different connections (hence also outside the above
+        # transaction)
+        if pending_references:
+            # Pass the references to connection-specific handler.
+            connection.creation.post_create_pending_references(pending_references)
 
-        # Send the post_syncdb signal, so individual apps can do whatever they need
-        # to do at this point.
+        # Send the post_syncdb signal, so individual apps can do whatever they
+        # need to do at this point.
         emit_post_sync_signal(created_models, verbosity, interactive, db)
 
         # The connection may have been closed by a syncdb handler.
Index: django/core/management/commands/loaddata.py
===================================================================
--- django/core/management/commands/loaddata.py	(revision 17914)
+++ django/core/management/commands/loaddata.py	(working copy)
@@ -218,8 +218,8 @@
 
             # Since we disabled constraint checks, we must manually check for
             # any invalid keys that might have been added
-            table_names = [model._meta.db_table for model in models]
-            connection.check_constraints(table_names=table_names)
+            qualified_names = [model._meta.qualified_name for model in models]
+            connection.check_constraints(table_names=qualified_names)
 
         except (SystemExit, KeyboardInterrupt):
             raise
Index: django/core/management/commands/inspectdb.py
===================================================================
--- django/core/management/commands/inspectdb.py	(revision 17914)
+++ django/core/management/commands/inspectdb.py	(working copy)
@@ -27,7 +27,7 @@
     def handle_inspection(self, options):
         connection = connections[options.get('database')]
 
-        table2model = lambda table_name: table_name.title().replace('_', '').replace(' ', '').replace('-', '')
+        table2model = lambda qname: qname[1].title().replace('_', '').replace(' ', '').replace('-', '')
 
         cursor = connection.cursor()
         yield "# This is an auto-generated Django model module."
@@ -41,17 +41,18 @@
         yield ''
         yield 'from %s import models' % self.db_module
         yield ''
-        for table_name in connection.introspection.get_table_list(cursor):
-            yield 'class %s(models.Model):' % table2model(table_name)
+        inspect = connection.introspection
+        for qname in inspect.get_visible_tables_list(cursor):
+            yield 'class %s(models.Model):' % table2model(qname)
             try:
-                relations = connection.introspection.get_relations(cursor, table_name)
+                relations = inspect.get_relations(cursor, qname)
             except NotImplementedError:
                 relations = {}
             try:
-                indexes = connection.introspection.get_indexes(cursor, table_name)
+                indexes = inspect.get_indexes(cursor, qname)
             except NotImplementedError:
                 indexes = {}
-            for i, row in enumerate(connection.introspection.get_table_description(cursor, table_name)):
+            for i, row in enumerate(inspect.get_table_description(cursor, qname)):
                 column_name = row[0]
                 att_name = column_name.lower()
                 comment_notes = [] # Holds Field notes, to be displayed in a Python comment.
@@ -82,7 +83,7 @@
                     comment_notes.append('Field name made lowercase.')
 
                 if i in relations:
-                    rel_to = relations[i][1] == table_name and "'self'" or table2model(relations[i][1])
+                    rel_to = relations[i][1] == qname and "'self'" or table2model(relations[i][1])
                     field_type = 'ForeignKey(%s' % rel_to
                     if att_name.endswith('_id'):
                         att_name = att_name[:-3]
@@ -91,7 +92,7 @@
                 else:
                     # Calling `get_field_type` to get the field type string and any
                     # additional paramters and notes.
-                    field_type, field_params, field_notes = self.get_field_type(connection, table_name, row)
+                    field_type, field_params, field_notes = self.get_field_type(connection, qname, row)
                     extra_params.update(field_params)
                     comment_notes.extend(field_notes)
 
@@ -118,7 +119,6 @@
                     extra_params['blank'] = True
                     if not field_type in ('TextField(', 'CharField('):
                         extra_params['null'] = True
-
                 field_desc = '%s = models.%s' % (att_name, field_type)
                 if extra_params:
                     if not field_desc.endswith('('):
@@ -128,10 +128,10 @@
                 if comment_notes:
                     field_desc += ' # ' + ' '.join(comment_notes)
                 yield '    %s' % field_desc
-            for meta_line in self.get_meta(table_name):
+            for meta_line in self.get_meta(qname):
                 yield meta_line
 
-    def get_field_type(self, connection, table_name, row):
+    def get_field_type(self, connection, qname, row):
         """
         Given the database connection, the table name, and the cursor row
         description, this routine will return the given field type name, as
@@ -162,12 +162,13 @@
 
         return field_type, field_params, field_notes
 
-    def get_meta(self, table_name):
+    def get_meta(self, qname):
         """
         Return a sequence comprising the lines of code necessary
         to construct the inner Meta class for the model corresponding
         to the given database table name.
         """
         return ['    class Meta:',
-                '        db_table = %r' % table_name,
+                '        db_table = %r' % qname[1],
+                '        db_schema = %r' % qname[0] or 'None',
                 '']
Index: django/core/management/validation.py
===================================================================
--- django/core/management/validation.py	(revision 17914)
+++ django/core/management/validation.py	(working copy)
@@ -19,7 +19,6 @@
     validates all models of all installed apps. Writes errors, if any, to outfile.
     Returns number of errors.
     """
-    from django.conf import settings
     from django.db import models, connection
     from django.db.models.loading import get_app_errors
     from django.db.models.fields.related import RelatedObject
@@ -32,7 +31,7 @@
 
     for cls in models.get_models(app):
         opts = cls._meta
-
+        
         # Do field-specific validation.
         for f in opts.local_fields:
             if f.name == 'id' and not f.primary_key and opts.pk.name == 'id':
Index: django/core/management/sql.py
===================================================================
--- django/core/management/sql.py	(revision 17914)
+++ django/core/management/sql.py	(working copy)
@@ -3,12 +3,11 @@
 
 from django.conf import settings
 from django.core.management.base import CommandError
-from django.db import models
+from django.db import models, QName
 from django.db.models import get_models
 
 def sql_create(app, style, connection):
     "Returns a list of the CREATE TABLE SQL statements for the given app."
-
     if connection.settings_dict['ENGINE'] == 'django.db.backends.dummy':
         # This must be the "dummy" database backend, which means the user
         # hasn't set ENGINE for the database.
@@ -23,11 +22,16 @@
     # we can be conservative).
     app_models = models.get_models(app, include_auto_created=True)
     final_output = []
-    tables = connection.introspection.table_names()
+    tables = connection.introspection.all_qualified_names()
     known_models = set([model for model in connection.introspection.installed_models(tables) if model not in app_models])
     pending_references = {}
 
     for model in app_models:
+        schema = connection.convert_schema(model._meta.db_schema)
+        if schema:
+            output = connection.creation.sql_create_schema(schema, style)
+            if output:
+                final_output.append(output)
         output, references = connection.creation.sql_create_model(model, style, known_models)
         final_output.extend(output)
         for refto, refs in references.items():
@@ -63,7 +67,7 @@
 
     # Figure out which tables already exist
     if cursor:
-        table_names = connection.introspection.get_table_list(cursor)
+        table_names = connection.introspection.all_qualified_names(converted=True)
     else:
         table_names = []
 
@@ -75,7 +79,7 @@
     references_to_delete = {}
     app_models = models.get_models(app, include_auto_created=True)
     for model in app_models:
-        if cursor and connection.introspection.table_name_converter(model._meta.db_table) in table_names:
+        if cursor and connection.introspection.qname_converter(model._meta.qualified_name) in table_names:
             # The table exists, so it needs to be dropped
             opts = model._meta
             for f in opts.local_fields:
@@ -85,7 +89,7 @@
             to_delete.add(model)
 
     for model in app_models:
-        if connection.introspection.table_name_converter(model._meta.db_table) in table_names:
+        if connection.introspection.qname_converter(model._meta.qualified_name) in table_names:
             output.extend(connection.creation.sql_destroy_model(model, references_to_delete, style))
 
     # Close database connection explicitly, in case this output is being piped
@@ -106,7 +110,9 @@
     if only_django:
         tables = connection.introspection.django_table_names(only_existing=True)
     else:
-        tables = connection.introspection.table_names()
+        tables = connection.introspection.all_qualified_names()
+    if [t for t in tables if not isinstance(t, QName)]:
+        import ipdb; ipdb.set_trace()
     statements = connection.ops.sql_flush(
         style, tables, connection.introspection.sequence_list()
     )
@@ -145,7 +151,7 @@
     if opts.managed:
         post_sql_fields = [f for f in opts.local_fields if hasattr(f, 'post_create_sql')]
         for f in post_sql_fields:
-            output.extend(f.post_create_sql(style, model._meta.db_table))
+            output.extend(f.post_create_sql(style, model._meta.qualified_name))
 
     # Some backends can't execute more than one SQL statement at a time,
     # so split into separate statements.
Index: django/contrib/contenttypes/generic.py
===================================================================
--- django/contrib/contenttypes/generic.py	(revision 17914)
+++ django/contrib/contenttypes/generic.py	(working copy)
@@ -173,6 +173,12 @@
     def m2m_db_table(self):
         return self.rel.to._meta.db_table
 
+    def m2m_qualified_name(self):
+        return self.rel.to._meta.qualified_name
+
+    def m2m_db_schema(self):
+        return self.rel.to._meta.db_schema
+
     def m2m_column_name(self):
         return self.object_id_field_name
 
Index: django/test/simple.py
===================================================================
--- django/test/simple.py	(revision 17914)
+++ django/test/simple.py	(working copy)
@@ -271,6 +271,8 @@
         dependencies = {}
         for alias in connections:
             connection = connections[alias]
+            if not connection.settings_dict['TEST_SCHEMA_PREFIX'] is None:
+                connection.settings_dict['TEST_SCHEMA_PREFIX'] = '%s_' % alias
             if connection.settings_dict['TEST_MIRROR']:
                 # If the database is marked as a test mirror, save
                 # the alias.
@@ -301,20 +303,20 @@
             test_databases.items(), dependencies):
             # Actually create the database for the first connection
             connection = connections[aliases[0]]
-            old_names.append((connection, db_name, True))
-            test_db_name = connection.creation.create_test_db(
+            test_db_name, created_schemas = connection.creation.create_test_db(
                 self.verbosity, autoclobber=not self.interactive)
+            old_names.append((connection, db_name, True, created_schemas))
             for alias in aliases[1:]:
                 connection = connections[alias]
                 if db_name:
-                    old_names.append((connection, db_name, False))
+                    old_names.append((connection, db_name, False, []))
                     connection.settings_dict['NAME'] = test_db_name
                 else:
                     # If settings_dict['NAME'] isn't defined, we have a backend
                     # where the name isn't important -- e.g., SQLite, which
                     # uses :memory:. Force create the database instead of
                     # assuming it's a duplicate.
-                    old_names.append((connection, db_name, True))
+                    old_names.append((connection, db_name, True, []))
                     connection.creation.create_test_db(
                         self.verbosity, autoclobber=not self.interactive)
 
@@ -335,9 +337,9 @@
         Destroys all the non-mirror databases.
         """
         old_names, mirrors = old_config
-        for connection, old_name, destroy in old_names:
+        for connection, old_name, destroy, created_schemas in old_names:
             if destroy:
-                connection.creation.destroy_test_db(old_name, self.verbosity)
+                connection.creation.destroy_test_db(old_name, created_schemas, self.verbosity)
 
     def teardown_test_environment(self, **kwargs):
         unittest.removeHandler()
Index: tests/modeltests/raw_query/tests.py
===================================================================
--- tests/modeltests/raw_query/tests.py	(revision 17914)
+++ tests/modeltests/raw_query/tests.py	(working copy)
@@ -2,7 +2,8 @@
 
 from datetime import date
 
-from django.db.models.sql.query import InvalidQuery
+from django.db.models.query_utils import InvalidQuery
+from django.db import connection
 from django.test import TestCase
 
 from .models import Author, Book, Coffee, Reviewer, FriendlyAuthor
@@ -59,7 +60,7 @@
         """
         Basic test of raw query with a simple database query
         """
-        query = "SELECT * FROM raw_query_author"
+        query = "SELECT * FROM %s" % connection.qname(Author)
         authors = Author.objects.all()
         self.assertSuccessfulRawQuery(Author, query, authors)
 
@@ -68,7 +69,7 @@
         Raw queries are lazy: they aren't actually executed until they're
         iterated over.
         """
-        q = Author.objects.raw('SELECT * FROM raw_query_author')
+        q = Author.objects.raw('SELECT * FROM %s' % connection.qname(Author))
         self.assertTrue(q.query.cursor is None)
         list(q)
         self.assertTrue(q.query.cursor is not None)
@@ -77,7 +78,7 @@
         """
         Test of a simple raw query against a model containing a foreign key
         """
-        query = "SELECT * FROM raw_query_book"
+        query = "SELECT * FROM %s" % connection.qname(Book)
         books = Book.objects.all()
         self.assertSuccessfulRawQuery(Book, query, books)
 
@@ -86,7 +87,7 @@
         Test of a simple raw query against a model containing a field with
         db_column defined.
         """
-        query = "SELECT * FROM raw_query_coffee"
+        query = "SELECT * FROM %s" % connection.qname(Coffee)
         coffees = Coffee.objects.all()
         self.assertSuccessfulRawQuery(Coffee, query, coffees)
 
@@ -102,7 +103,7 @@
         )
 
         for select in selects:
-            query = "SELECT %s FROM raw_query_author" % select
+            query = "SELECT %s FROM %s" % (select, connection.qname(Author))
             authors = Author.objects.all()
             self.assertSuccessfulRawQuery(Author, query, authors)
 
@@ -111,7 +112,8 @@
         Test of raw query's optional ability to translate unexpected result
         column names to specific model fields
         """
-        query = "SELECT first_name AS first, last_name AS last, dob, id FROM raw_query_author"
+        query = ("SELECT first_name AS first, last_name AS last, dob, id FROM %s"
+                 % connection.qname(Author))
         translations = {'first': 'first_name', 'last': 'last_name'}
         authors = Author.objects.all()
         self.assertSuccessfulRawQuery(Author, query, authors, translations=translations)
@@ -120,7 +122,7 @@
         """
         Test passing optional query parameters
         """
-        query = "SELECT * FROM raw_query_author WHERE first_name = %s"
+        query = "SELECT * FROM %s WHERE first_name = %%s" % connection.qname(Author)
         author = Author.objects.all()[2]
         params = [author.first_name]
         results = list(Author.objects.raw(query, params=params))
@@ -132,7 +134,7 @@
         """
         Test of a simple raw query against a model containing a m2m field
         """
-        query = "SELECT * FROM raw_query_reviewer"
+        query = "SELECT * FROM %s" % connection.qname(Reviewer)
         reviewers = Reviewer.objects.all()
         self.assertSuccessfulRawQuery(Reviewer, query, reviewers)
 
@@ -140,20 +142,20 @@
         """
         Test to insure that extra translations are ignored.
         """
-        query = "SELECT * FROM raw_query_author"
+        query = "SELECT * FROM %s" % connection.qname(Author)
         translations = {'something': 'else'}
         authors = Author.objects.all()
         self.assertSuccessfulRawQuery(Author, query, authors, translations=translations)
 
     def testMissingFields(self):
-        query = "SELECT id, first_name, dob FROM raw_query_author"
+        query = "SELECT id, first_name, dob FROM %s" % connection.qname(Author)
         for author in Author.objects.raw(query):
             self.assertNotEqual(author.first_name, None)
             # last_name isn't given, but it will be retrieved on demand
             self.assertNotEqual(author.last_name, None)
 
     def testMissingFieldsWithoutPK(self):
-        query = "SELECT first_name, dob FROM raw_query_author"
+        query = "SELECT first_name, dob FROM %s" % connection.qname(Author)
         try:
             list(Author.objects.raw(query))
             self.fail('Query without primary key should fail')
@@ -161,7 +163,7 @@
             pass
 
     def testAnnotations(self):
-        query = "SELECT a.*, count(b.id) as book_count FROM raw_query_author a LEFT JOIN raw_query_book b ON a.id = b.author_id GROUP BY a.id, a.first_name, a.last_name, a.dob ORDER BY a.id"
+        query = "SELECT a.*, count(b.id) as book_count FROM %s a LEFT JOIN %s b ON a.id = b.author_id GROUP BY a.id, a.first_name, a.last_name, a.dob ORDER BY a.id" % (connection.qname(Author), connection.qname(Book))
         expected_annotations = (
             ('book_count', 3),
             ('book_count', 0),
@@ -172,12 +174,12 @@
         self.assertSuccessfulRawQuery(Author, query, authors, expected_annotations)
 
     def testWhiteSpaceQuery(self):
-        query = "    SELECT * FROM raw_query_author"
+        query = "    SELECT * FROM %s" % connection.qname(Author)
         authors = Author.objects.all()
         self.assertSuccessfulRawQuery(Author, query, authors)
 
     def testMultipleIterations(self):
-        query = "SELECT * FROM raw_query_author"
+        query = "SELECT * FROM %s" % connection.qname(Author)
         normal_authors = Author.objects.all()
         raw_authors = Author.objects.raw(query)
 
@@ -197,7 +199,7 @@
 
     def testGetItem(self):
         # Indexing on RawQuerySets
-        query = "SELECT * FROM raw_query_author ORDER BY id ASC"
+        query = "SELECT * FROM %s ORDER BY id ASC" % connection.qname(Author)
         third_author = Author.objects.raw(query)[2]
         self.assertEqual(third_author.first_name, 'Bob')
 
@@ -211,12 +213,12 @@
         # Wesley was bron
         f = FriendlyAuthor.objects.create(first_name="Wesley", last_name="Chun",
             dob=date(1962, 10, 28))
-        query = "SELECT * FROM raw_query_friendlyauthor"
+        query = "SELECT * FROM %s" % connection.qname(FriendlyAuthor)
         self.assertEqual(
             [o.pk for o in FriendlyAuthor.objects.raw(query)], [f.pk]
         )
 
     def test_query_count(self):
         self.assertNumQueries(1,
-            list, Author.objects.raw("SELECT * FROM raw_query_author")
+            list, Author.objects.raw("SELECT * FROM %s" % connection.qname(Author))
         )
Index: tests/modeltests/timezones/tests.py
===================================================================
--- tests/modeltests/timezones/tests.py	(revision 17914)
+++ tests/modeltests/timezones/tests.py	(working copy)
@@ -264,7 +264,9 @@
         dt = datetime.datetime(2011, 9, 1, 13, 20, 30)
         event = Event.objects.create(dt=dt)
         self.assertQuerysetEqual(
-                Event.objects.raw('SELECT * FROM timezones_event WHERE dt = %s', [dt]),
+                Event.objects.raw('SELECT * FROM %s WHERE dt = %%s'
+                                  % connection.qname(Event),
+                                  [dt]),
                 [event],
                 transform=lambda d: d)
 
@@ -476,7 +478,8 @@
         dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
         event = Event.objects.create(dt=dt)
         self.assertQuerysetEqual(
-                Event.objects.raw('SELECT * FROM timezones_event WHERE dt = %s', [dt]),
+                Event.objects.raw('SELECT * FROM %s WHERE dt = %%s'
+                                  % connection.qname(Event), [dt]),
                 [event],
                 transform=lambda d: d)
 
Index: tests/modeltests/proxy_models/tests.py
===================================================================
--- tests/modeltests/proxy_models/tests.py	(revision 17914)
+++ tests/modeltests/proxy_models/tests.py	(working copy)
@@ -3,9 +3,9 @@
 from django.contrib.contenttypes.models import ContentType
 from django.core import management
 from django.core.exceptions import FieldError
-from django.db import models, DEFAULT_DB_ALIAS
+from django.db import models, DEFAULT_DB_ALIAS, transaction, IntegrityError
 from django.db.models import signals
-from django.test import TestCase
+from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
 
 
 from .models import (MyPerson, Person, StatusPerson, LowerStatusPerson,
@@ -326,3 +326,17 @@
         management.call_command('loaddata', 'mypeople.json', verbosity=0, commit=False)
         p = MyPerson.objects.get(pk=100)
         self.assertEqual(p.name, 'Elvis Presley')
+
+class TransactionalProxyModelTests(TransactionTestCase):
+    @skipUnlessDBFeature('supports_foreign_keys')
+    def test_proxy_fk(self):
+        """
+        Test that the DB contains proper foreign keys for proxy model references.
+        """
+        @transaction.commit_on_success
+        def create_failing_pk():
+            t = TrackerUser.objects.create(status='bar')
+            Improvement.objects.create(summary='foof', version='foof',
+                                       reporter_id=1, associated_bug_id=1,
+                                       assignee=t)
+        self.assertRaises(IntegrityError, create_failing_pk)
Index: tests/modeltests/select_for_update/tests.py
===================================================================
--- tests/modeltests/select_for_update/tests.py	(revision 17914)
+++ tests/modeltests/select_for_update/tests.py	(working copy)
@@ -36,7 +36,6 @@
         # issuing a SELECT ... FOR UPDATE will block.
         new_connections = ConnectionHandler(settings.DATABASES)
         self.new_connection = new_connections[DEFAULT_DB_ALIAS]
-
         # We need to set settings.DEBUG to True so we can capture
         # the output SQL to examine.
         self._old_debug = settings.DEBUG
@@ -62,11 +61,11 @@
         # end_blocking_transaction() should be called.
         self.cursor = self.new_connection.cursor()
         sql = 'SELECT * FROM %(db_table)s %(for_update)s;' % {
-            'db_table': Person._meta.db_table,
+            'db_table': self.new_connection.qname(Person),
             'for_update': self.new_connection.ops.for_update_sql(),
             }
         self.cursor.execute(sql, ())
-        result = self.cursor.fetchone()
+        self.cursor.fetchone()
 
     def end_blocking_transaction(self):
         # Roll back the blocking transaction.
@@ -241,7 +240,7 @@
                 list(
                     Person.objects.raw(
                         'SELECT * FROM %s %s' % (
-                            Person._meta.db_table,
+                            connection.qname(Person),
                             connection.ops.for_update_sql(nowait=True)
                         )
                     )
Index: tests/modeltests/unmanaged_models/tests.py
===================================================================
--- tests/modeltests/unmanaged_models/tests.py	(revision 17914)
+++ tests/modeltests/unmanaged_models/tests.py	(working copy)
@@ -48,14 +48,16 @@
         """
         The intermediary table between two unmanaged models should not be created.
         """
-        table = Unmanaged2._meta.get_field('mm').m2m_db_table()
-        tables = connection.introspection.table_names()
-        self.assertTrue(table not in tables, "Table '%s' should not exist, but it does." % table)
+        conv = connection.introspection.qname_converter
+        table = conv(Unmanaged2.mm.through._meta.qualified_name)
+        tables = connection.introspection.all_qualified_names()
+        self.assertTrue(table not in tables, "Table '%s' should not exist, but it does." % table[1])
 
     def test_many_to_many_between_unmanaged_and_managed(self):
         """
         An intermediary table between a managed and an unmanaged model should be created.
         """
-        table = Managed1._meta.get_field('mm').m2m_db_table()
-        tables = connection.introspection.table_names()
-        self.assertTrue(table in tables, "Table '%s' does not exist." % table)
+        conv = connection.introspection.qname_converter
+        table = conv(Managed1.mm.through._meta.qualified_name)
+        tables = connection.introspection.all_qualified_names()
+        self.assertTrue(table in tables, "Table '%s' does not exist." % table[1])
Index: tests/modeltests/many_to_one/tests.py
===================================================================
--- tests/modeltests/many_to_one/tests.py	(revision 17914)
+++ tests/modeltests/many_to_one/tests.py	(working copy)
@@ -4,6 +4,7 @@
 from datetime import datetime
 
 from django.core.exceptions import MultipleObjectsReturned
+from django.db import connection
 from django.test import TestCase
 from django.utils.translation import ugettext_lazy
 
@@ -169,7 +170,7 @@
         # The automatically joined table has a predictable name.
         self.assertQuerysetEqual(
             Article.objects.filter(reporter__first_name__exact='John').extra(
-                where=["many_to_one_reporter.last_name='Smith'"]),
+                where=["%s.last_name='Smith'" % connection.qname(Reporter)]),
             [
                 "<Article: John's second story>",
                 "<Article: This is a test>",
@@ -177,7 +178,8 @@
         # ... and should work fine with the unicode that comes out of forms.Form.cleaned_data
         self.assertQuerysetEqual(
             Article.objects.filter(reporter__first_name__exact='John'
-                                  ).extra(where=["many_to_one_reporter.last_name='%s'" % u'Smith']),
+                                  ).extra(where=["%s.last_name='%s'" %
+                                                 (connection.qname(Reporter), u'Smith')]),
             [
                 "<Article: John's second story>",
                 "<Article: This is a test>",
Index: tests/modeltests/prefetch_related/tests.py
===================================================================
--- tests/modeltests/prefetch_related/tests.py	(revision 17914)
+++ tests/modeltests/prefetch_related/tests.py	(working copy)
@@ -372,8 +372,9 @@
             l = [a.authorwithage for a in Author.objects.prefetch_related('authorwithage')]
 
         # Regression for #18090: the prefetching query must include an IN clause.
-        self.assertIn('authorwithage', connection.queries[-1]['sql'])
-        self.assertIn(' IN ', connection.queries[-1]['sql'])
+        executed_sql = connection.queries[-1]['sql'].lower()
+        self.assertIn('authorwithage', executed_sql)
+        self.assertIn(' in ', executed_sql)
 
         self.assertEqual(l, [a.authorwithage for a in Author.objects.all()])
 
Index: tests/modeltests/select_related/tests.py
===================================================================
--- tests/modeltests/select_related/tests.py	(revision 17914)
+++ tests/modeltests/select_related/tests.py	(working copy)
@@ -1,5 +1,6 @@
 from __future__ import absolute_import
 
+from django.db import connection
 from django.test import TestCase
 
 from .models import Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species
@@ -120,7 +121,7 @@
 
     def test_select_related_with_extra(self):
         s = Species.objects.all().select_related(depth=1)\
-            .extra(select={'a': 'select_related_species.id + 10'})[0]
+            .extra(select={'a': '%s.id + 10' % connection.qname(Species)})[0]
         self.assertEqual(s.id + 10, s.a)
 
     def test_certain_fields(self):
Index: tests/modeltests/custom_methods/models.py
===================================================================
--- tests/modeltests/custom_methods/models.py	(revision 17914)
+++ tests/modeltests/custom_methods/models.py	(working copy)
@@ -31,8 +31,9 @@
         cursor = connection.cursor()
         cursor.execute("""
             SELECT id, headline, pub_date
-            FROM custom_methods_article
-            WHERE pub_date = %s
-                AND id != %s""", [connection.ops.value_to_db_date(self.pub_date),
-                                  self.id])
+            FROM %s
+            WHERE pub_date = %%s
+                AND id != %%s""" % connection.qname(self),
+                       [connection.ops.value_to_db_date(self.pub_date),
+                        self.id])
         return [self.__class__(*row) for row in cursor.fetchall()]
Index: tests/modeltests/transactions/tests.py
===================================================================
--- tests/modeltests/transactions/tests.py	(revision 17914)
+++ tests/modeltests/transactions/tests.py	(working copy)
@@ -164,7 +164,8 @@
 class TransactionRollbackTests(TransactionTestCase):
     def execute_bad_sql(self):
         cursor = connection.cursor()
-        cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');")
+        tbl = connection.qname(Reporter)
+        cursor.execute("INSERT INTO %s (first_name, last_name) VALUES ('Douglas', 'Adams');" % tbl)
         transaction.set_dirty()
 
     @skipUnlessDBFeature('requires_rollback_on_dirty_transaction')
@@ -305,6 +306,7 @@
         with self.assertRaises(IntegrityError):
             with transaction.commit_on_success():
                 cursor = connection.cursor()
-                cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');")
+                tbl = connection.qname(Reporter)
+                cursor.execute("INSERT INTO %s (first_name, last_name) VALUES ('Douglas', 'Adams');" % tbl)
                 transaction.set_dirty()
         transaction.rollback()
Index: tests/regressiontests/transactions_regress/tests.py
===================================================================
--- tests/regressiontests/transactions_regress/tests.py	(revision 17914)
+++ tests/regressiontests/transactions_regress/tests.py	(working copy)
@@ -25,7 +25,8 @@
         def raw_sql():
             "Write a record using raw sql under a commit_on_success decorator"
             cursor = connection.cursor()
-            cursor.execute("INSERT into transactions_regress_mod (id,fld) values (17,18)")
+            tbl = connection.qname(Mod)
+            cursor.execute("INSERT into %s (id,fld) values (17,18)" % tbl)
 
         raw_sql()
         # Rollback so that if the decorator didn't commit, the record is unwritten
@@ -116,9 +117,10 @@
             be committed.
             """
             cursor = connection.cursor()
-            cursor.execute("INSERT into transactions_regress_mod (id,fld) values (1,2)")
+            tbl = connection.qname(Mod)
+            cursor.execute("INSERT into %s (id,fld) values (1,2)" % tbl)
             transaction.rollback()
-            cursor.execute("INSERT into transactions_regress_mod (id,fld) values (1,2)")
+            cursor.execute("INSERT into %s (id,fld) values (1,2)" % tbl)
 
         reuse_cursor_ref()
         # Rollback so that if the decorator didn't commit, the record is unwritten
Index: tests/regressiontests/queries/tests.py
===================================================================
--- tests/regressiontests/queries/tests.py	(revision 17914)
+++ tests/regressiontests/queries/tests.py	(working copy)
@@ -6,6 +6,7 @@
 
 from django.conf import settings
 from django.core.exceptions import FieldError
+from django.contrib.sites.models import Site
 from django.db import DatabaseError, connection, connections, DEFAULT_DB_ALIAS
 from django.db.models import Count
 from django.db.models.query import Q, ITER_CHUNK_SIZE, EmptyQuerySet
@@ -387,7 +388,7 @@
 
     def test_ticket2496(self):
         self.assertQuerysetEqual(
-            Item.objects.extra(tables=['queries_author']).select_related().order_by('name')[:1],
+            Item.objects.extra(tables=[Author._meta.qualified_name]).select_related().order_by('name')[:1],
             ['<Item: four>']
         )
 
@@ -507,7 +508,11 @@
         self.assertEqual(d, {'a': u'one', 'b': u'two'})
 
         # Order by the number of tags attached to an item.
-        l = Item.objects.extra(select={'count': 'select count(*) from queries_item_tags where queries_item_tags.item_id = queries_item.id'}).order_by('-count')
+        l = Item.objects.extra(select={
+            'count':'select count(*) from %s where %s.item_id = %s.id' %
+            (connection.qname(Item.tags.through), connection.qname(Item.tags.through),
+             connection.qname(Item))
+        }).order_by('-count')
         self.assertEqual([o.count for o in l], [2, 2, 1, 0])
 
     def test_ticket6154(self):
@@ -577,11 +582,15 @@
 
     def test_ticket7098(self):
         # Make sure semi-deprecated ordering by related models syntax still
-        # works.
-        self.assertValueQuerysetEqual(
-            Item.objects.values('note__note').order_by('queries_note.note', 'id'),
-            [{'note__note': u'n2'}, {'note__note': u'n3'}, {'note__note': u'n3'}, {'note__note': u'n3'}]
-        )
+        # works.        
+        # Skip this test if schema support is in effect - there is little point to fix the
+        # deprecated .order_by() notation to support schemas.
+        if not connection.schema:
+            self.assertValueQuerysetEqual(
+                # Need to remove the quotes from the table name for this test...
+                Item.objects.values('note__note').order_by('%s.note' % connection.qname(Note)[1:-1], 'id'),
+                [{'note__note': u'n2'}, {'note__note': u'n3'}, {'note__note': u'n3'}, {'note__note': u'n3'}]
+            )
 
     def test_ticket7096(self):
         # Make sure exclude() with multiple conditions continues to work.
@@ -1216,7 +1225,8 @@
         # Ordering of extra() pieces is possible, too and you can mix extra
         # fields and model fields in the ordering.
         self.assertQuerysetEqual(
-            Ranking.objects.extra(tables=['django_site'], order_by=['-django_site.id', 'rank']),
+            Ranking.objects.extra(tables=[Site._meta.qualified_name],
+                                  order_by=['-%s.id' % connection.qname(Site), 'rank']),
             ['<Ranking: 1: a3>', '<Ranking: 2: a2>', '<Ranking: 3: a1>']
         )
 
@@ -1251,7 +1261,7 @@
 
     def test_ticket7045(self):
         # Extra tables used to crash SQL construction on the second use.
-        qs = Ranking.objects.extra(tables=['django_site'])
+        qs = Ranking.objects.extra(tables=[connection.qname(Site)])
         qs.query.get_compiler(qs.db).as_sql()
         # test passes if this doesn't raise an exception.
         qs.query.get_compiler(qs.db).as_sql()
Index: tests/regressiontests/delete_regress/tests.py
===================================================================
--- tests/regressiontests/delete_regress/tests.py	(revision 17914)
+++ tests/regressiontests/delete_regress/tests.py	(working copy)
@@ -17,6 +17,8 @@
     def setUp(self):
         # Create a second connection to the default database
         conn_settings = settings.DATABASES[DEFAULT_DB_ALIAS]
+        # TODO: there must be a better way to do this copying. .deepcopy()
+        # perhaps?
         self.conn2 = backend.DatabaseWrapper({
             'HOST': conn_settings['HOST'],
             'NAME': conn_settings['NAME'],
@@ -25,6 +27,9 @@
             'PORT': conn_settings['PORT'],
             'USER': conn_settings['USER'],
             'TIME_ZONE': settings.TIME_ZONE,
+            'SCHEMA': conn_settings['SCHEMA'],
+            'TEST_SCHEMA_PREFIX': conn_settings['TEST_SCHEMA_PREFIX'],
+            'TEST_SCHEMAS': conn_settings['TEST_SCHEMAS'],
         })
 
         # Put both DB connections into managed transaction mode
@@ -55,7 +60,7 @@
 
         # Delete something using connection 2.
         cursor2 = self.conn2.cursor()
-        cursor2.execute('DELETE from delete_regress_book WHERE id=1')
+        cursor2.execute('DELETE from %s WHERE id=1' % self.conn2.qname(Book))
         self.conn2._commit()
 
         # Now perform a queryset delete that covers the object
Index: tests/regressiontests/backends/tests.py
===================================================================
--- tests/regressiontests/backends/tests.py	(revision 17914)
+++ tests/regressiontests/backends/tests.py	(working copy)
@@ -9,7 +9,7 @@
 from django.core.management.color import no_style
 from django.core.exceptions import ImproperlyConfigured
 from django.db import (backend, connection, connections, DEFAULT_DB_ALIAS,
-    IntegrityError, transaction)
+    IntegrityError, transaction, QName)
 from django.db.backends.signals import connection_created
 from django.db.backends.postgresql_psycopg2 import version as pg_version
 from django.db.utils import ConnectionHandler, DatabaseError, load_backend
@@ -129,7 +129,7 @@
         "An executemany call with too many/not enough parameters will raise an exception (Refs #12612)"
         cursor = connection.cursor()
         query = ('INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' % (
-            connection.introspection.table_name_converter('backends_square'),
+            connection.introspection.identifier_converter('backends_square'),
             connection.ops.quote_name('root'),
             connection.ops.quote_name('square')
         ))
@@ -169,13 +169,13 @@
         VLM = models.VeryLongModelNameZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
         VLM_m2m = VLM.m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.through
         tables = [
-            VLM._meta.db_table,
-            VLM_m2m._meta.db_table,
+            VLM._meta.qualified_name,
+            VLM_m2m._meta.qualified_name
         ]
         sequences = [
             {
                 'column': VLM._meta.pk.column,
-                'table': VLM._meta.db_table
+                'qname': QName(None, VLM._meta.db_table, False),
             },
         ]
         cursor = connection.cursor()
@@ -313,7 +313,7 @@
     def create_squares_with_executemany(self, args):
         cursor = connection.cursor()
         opts = models.Square._meta
-        tbl = connection.introspection.table_name_converter(opts.db_table)
+        tbl = connection.qname(models.Square)
         f1 = connection.ops.quote_name(opts.get_field('root').column)
         f2 = connection.ops.quote_name(opts.get_field('square').column)
         query = 'INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' % (tbl, f1, f2)
@@ -358,7 +358,7 @@
         opts2 = models.Person._meta
         f3, f4 = opts2.get_field('first_name'), opts2.get_field('last_name')
         query2 = ('SELECT %s, %s FROM %s ORDER BY %s'
-          % (qn(f3.column), qn(f4.column), connection.introspection.table_name_converter(opts2.db_table),
+          % (qn(f3.column), qn(f4.column), connection.qname(models.Person),
              qn(f3.column)))
         cursor = connection.cursor()
         cursor.execute(query2)
@@ -375,7 +375,7 @@
     def test_duplicate_table_error(self):
         """ Test that creating an existing table returns a DatabaseError """
         cursor = connection.cursor()
-        query = 'CREATE TABLE %s (id INTEGER);' % models.Article._meta.db_table
+        query = 'CREATE TABLE %s (id INTEGER);' % connection.qname(models.Article)
         with self.assertRaises(DatabaseError):
             cursor.execute(query)
 
Index: tests/regressiontests/inspectdb/tests.py
===================================================================
--- tests/regressiontests/inspectdb/tests.py	(revision 17914)
+++ tests/regressiontests/inspectdb/tests.py	(working copy)
@@ -1,3 +1,5 @@
+from __future__ import absolute_import
+
 from StringIO import StringIO
 
 from django.core.management import call_command
@@ -2,2 +4,3 @@
 from django.test import TestCase, skipUnlessDBFeature
+from .models import People
 
@@ -8,14 +11,17 @@
 
     @skipUnlessDBFeature('can_introspect_foreign_keys')
     def test_attribute_name_not_python_keyword(self):
+        from django.db import connection
+        _, tbl, _ = connection.introspection.qname_converter(People._meta.qualified_name)
+        mname = ''.join(t.title() for t in tbl.split('_'))
         out = StringIO()
         call_command('inspectdb', stdout=out)
         error_message = "inspectdb generated an attribute name which is a python keyword"
-        self.assertNotIn("from = models.ForeignKey(InspectdbPeople)", out.getvalue(), msg=error_message)
-        self.assertIn("from_field = models.ForeignKey(InspectdbPeople)", out.getvalue())
-        self.assertIn("people_pk = models.ForeignKey(InspectdbPeople, primary_key=True)",
+        self.assertNotIn("from = models.ForeignKey(%s)" % mname, out.getvalue(), msg=error_message)
+        self.assertIn("from_field = models.ForeignKey(%s)" % mname, out.getvalue())
+        self.assertIn("people_pk = models.ForeignKey(%s, primary_key=True)" % mname,
             out.getvalue())
-        self.assertIn("people_unique = models.ForeignKey(InspectdbPeople, unique=True)",
+        self.assertIn("people_unique = models.ForeignKey(%s, unique=True)" % mname,
             out.getvalue())
         out.close()
 
Index: tests/regressiontests/extra_regress/tests.py
===================================================================
--- tests/regressiontests/extra_regress/tests.py	(revision 17914)
+++ tests/regressiontests/extra_regress/tests.py	(working copy)
@@ -3,6 +3,7 @@
 import datetime
 
 from django.contrib.auth.models import User
+from django.db import connection
 from django.test import TestCase
 from django.utils.datastructures import SortedDict
 
@@ -42,7 +43,7 @@
         # Queryset to match most recent revision:
         qs = RevisionableModel.objects.extra(
                 where=["%(table)s.id IN (SELECT MAX(rev.id) FROM %(table)s rev GROUP BY rev.base_id)" % {
-                    'table': RevisionableModel._meta.db_table,
+                    'table': connection.qname(RevisionableModel),
                 }]
         )
 
Index: tests/regressiontests/aggregation_regress/tests.py
===================================================================
--- tests/regressiontests/aggregation_regress/tests.py	(revision 17914)
+++ tests/regressiontests/aggregation_regress/tests.py	(working copy)
@@ -6,6 +6,7 @@
 from operator import attrgetter
 
 from django.core.exceptions import FieldError
+from django.db import connection
 from django.db.models import Count, Max, Avg, Sum, StdDev, Variance, F, Q
 from django.test import TestCase, Approximate, skipUnlessDBFeature
 
@@ -69,11 +70,11 @@
         #oracle doesn't support subqueries in group by clause
         shortest_book_sql = """
         SELECT name
-        FROM aggregation_regress_book b
-        WHERE b.publisher_id = aggregation_regress_publisher.id
+        FROM %s b
+        WHERE b.publisher_id = %s.id
         ORDER BY b.pages
         LIMIT 1
-        """
+        """ % (connection.qname(Book), connection.qname(Book))
         # tests that this query does not raise a DatabaseError due to the full
         # subselect being (erroneously) added to the GROUP BY parameters
         qs = Publisher.objects.extra(select={
Index: tests/regressiontests/admin_scripts/tests.py
===================================================================
--- tests/regressiontests/admin_scripts/tests.py	(revision 17914)
+++ tests/regressiontests/admin_scripts/tests.py	(working copy)
@@ -3,7 +3,6 @@
 advertised - especially with regards to the handling of the DJANGO_SETTINGS_MODULE
 and default settings.py files.
 """
-
 import os
 import re
 import shutil
@@ -13,14 +12,15 @@
 
 from django import conf, bin, get_version
 from django.conf import settings
+from django.db import connection
 from django.test.simple import DjangoTestSuiteRunner
 from django.utils import unittest
 from django.test import LiveServerTestCase
 
+from .models import Article
+
 test_dir = os.path.dirname(os.path.dirname(__file__))
-expected_query_re = re.compile(r'CREATE TABLE [`"]admin_scripts_article[`"]', re.IGNORECASE)
 
-
 class AdminScriptTestCase(unittest.TestCase):
     def write_settings(self, filename, apps=None, is_dir=False, sdict=None):
         test_dir = os.path.dirname(os.path.dirname(__file__))
@@ -844,6 +844,8 @@
     """
     def setUp(self):
         self.write_settings('alternate_settings.py')
+        tblname = connection.qname(Article)
+        self.expected_query_re = re.compile(r'CREATE TABLE %s' % tblname, re.IGNORECASE)
 
     def tearDown(self):
         self.remove_settings('alternate_settings.py')
@@ -859,14 +861,14 @@
         "alternate: manage.py builtin commands work with settings provided as argument"
         args = ['sqlall', '--settings=alternate_settings', 'admin_scripts']
         out, err = self.run_manage(args)
-        self.assertRegexpMatches(out, expected_query_re)
+        self.assertRegexpMatches(out, self.expected_query_re)
         self.assertNoOutput(err)
 
     def test_builtin_with_environment(self):
         "alternate: manage.py builtin commands work if settings are provided in the environment"
         args = ['sqlall', 'admin_scripts']
         out, err = self.run_manage(args, 'alternate_settings')
-        self.assertRegexpMatches(out, expected_query_re)
+        self.assertRegexpMatches(out, self.expected_query_re)
         self.assertNoOutput(err)
 
     def test_builtin_with_bad_settings(self):
Index: tests/regressiontests/introspection/tests.py
===================================================================
--- tests/regressiontests/introspection/tests.py	(revision 17914)
+++ tests/regressiontests/introspection/tests.py	(working copy)
@@ -2,7 +2,8 @@
 
 from functools import update_wrapper
 
-from django.db import connection
+from django.conf import settings
+from django.db import connection, QName
 from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature
 
 from .models import Reporter, Article
@@ -39,24 +40,29 @@
     __metaclass__ = IgnoreNotimplementedError
 
     def test_table_names(self):
-        tl = connection.introspection.table_names()
-        self.assertTrue(Reporter._meta.db_table in tl,
+        conv = connection.introspection.qname_converter
+        tl = connection.introspection.all_qualified_names()
+        self.assertTrue(conv(Reporter._meta.qualified_name) in tl,
                      "'%s' isn't in table_list()." % Reporter._meta.db_table)
-        self.assertTrue(Article._meta.db_table in tl,
+        self.assertTrue(conv(Article._meta.qualified_name) in tl,
                      "'%s' isn't in table_list()." % Article._meta.db_table)
 
     def test_django_table_names(self):
         cursor = connection.cursor()
-        cursor.execute('CREATE TABLE django_ixn_test_table (id INTEGER);')
+        tblname = connection.ops.qualified_name(
+            QName(None, 'django_ixn_test_table', False))
+        cursor.execute('CREATE TABLE %s (id INTEGER);' % tblname)
         tl = connection.introspection.django_table_names()
-        cursor.execute("DROP TABLE django_ixn_test_table;")
-        self.assertTrue('django_ixn_testcase_table' not in tl,
+        cursor.execute("DROP TABLE %s;" % tblname)
+        self.assertTrue(tblname not in tl,
                      "django_table_names() returned a non-Django table")
 
     def test_django_table_names_retval_type(self):
         # Ticket #15216
         cursor = connection.cursor()
-        cursor.execute('CREATE TABLE django_ixn_test_table (id INTEGER);')
+        tblname = connection.ops.qualified_name(
+            QName(None, 'django_ixn_test_table', False))
+        cursor.execute('CREATE TABLE %s (id INTEGER);' % tblname)
 
         tl = connection.introspection.django_table_names(only_existing=True)
         self.assertIs(type(tl), list)
@@ -65,25 +71,30 @@
         self.assertIs(type(tl), list)
 
     def test_installed_models(self):
-        tables = [Article._meta.db_table, Reporter._meta.db_table]
+        conv = connection.introspection.qname_converter
+        tables = [conv(Article._meta.qualified_name),
+                  conv(Reporter._meta.qualified_name)]
         models = connection.introspection.installed_models(tables)
         self.assertEqual(models, set([Article, Reporter]))
 
     def test_sequence_list(self):
         sequences = connection.introspection.sequence_list()
-        expected = {'table': Reporter._meta.db_table, 'column': 'id'}
+        qname = connection.introspection.qname_converter(Reporter._meta.qualified_name)
+        expected = {'qname': qname, 'column': 'id'}
         self.assertTrue(expected in sequences,
                      'Reporter sequence not found in sequence_list()')
 
     def test_get_table_description_names(self):
         cursor = connection.cursor()
-        desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table)
+        tbl = connection.introspection.qname_converter(Reporter._meta.qualified_name)
+        desc = connection.introspection.get_table_description(cursor, tbl)
         self.assertEqual([r[0] for r in desc],
                          [f.column for f in Reporter._meta.fields])
 
     def test_get_table_description_types(self):
         cursor = connection.cursor()
-        desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table)
+        tbl = connection.introspection.qname_converter(Reporter._meta.qualified_name)
+        desc = connection.introspection.get_table_description(cursor, tbl)
         self.assertEqual(
             [datatype(r[1], r) for r in desc],
             ['IntegerField', 'CharField', 'CharField', 'CharField', 'BigIntegerField']
@@ -95,7 +106,8 @@
     @skipIfDBFeature('interprets_empty_strings_as_nulls')
     def test_get_table_description_nullable(self):
         cursor = connection.cursor()
-        desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table)
+        tbl = connection.introspection.qname_converter(Reporter._meta.qualified_name)
+        desc = connection.introspection.get_table_description(cursor, tbl)
         self.assertEqual(
             [r[6] for r in desc],
             [False, False, False, False, True]
@@ -105,35 +117,51 @@
     @skipUnlessDBFeature('has_real_datatype')
     def test_postgresql_real_type(self):
         cursor = connection.cursor()
-        cursor.execute("CREATE TABLE django_ixn_real_test_table (number REAL);")
-        desc = connection.introspection.get_table_description(cursor, 'django_ixn_real_test_table')
-        cursor.execute('DROP TABLE django_ixn_real_test_table;')
+        tblname = connection.ops.qualified_name(
+            QName(None, 'django_ixn_real_test_table', False))
+        cursor.execute("CREATE TABLE %s (number REAL);" % tblname)
+        desc = connection.introspection.get_table_description(
+            cursor, QName(None, 'django_ixn_real_test_table', False))
+        cursor.execute('DROP TABLE %s;' % tblname)
         self.assertEqual(datatype(desc[0][1], desc[0]), 'FloatField')
 
     def test_get_relations(self):
         cursor = connection.cursor()
-        relations = connection.introspection.get_relations(cursor, Article._meta.db_table)
+        tbl = connection.introspection.qname_converter(Article._meta.qualified_name)
+        relations = connection.introspection.get_relations(cursor, tbl)
+        rep_tbl = connection.introspection.qname_converter(Reporter._meta.qualified_name)
 
         # Older versions of MySQL don't have the chops to report on this stuff,
         # so just skip it if no relations come back. If they do, though, we
         # should test that the response is correct.
         if relations:
             # That's {field_index: (field_index_other_table, other_table)}
-            self.assertEqual(relations, {3: (0, Reporter._meta.db_table)})
+            # We have a small problem here: the Reporter model is installed
+            # into the default schema (qualified_name[0] == None). The
+            # relation introspection is going to see it in that schema, but we
+            # do not know what that schema is. So, test everything except the
+            # schema.
+            # TODO: this testing logic is UGLY!
+            schema = connection.convert_schema(Reporter._meta.qualified_name[0])
+            self.assertTrue(3 in relations)
+            relations[3] = (relations[3][0], (schema, relations[3][1][1], True))
+            self.assertEqual(relations, {3: (0, rep_tbl)})
 
     def test_get_key_columns(self):
         cursor = connection.cursor()
-        key_columns = connection.introspection.get_key_columns(cursor, Article._meta.db_table)
-        self.assertEqual(key_columns, [(u'reporter_id', Reporter._meta.db_table, u'id')])
+        rep_tbl = connection.introspection.qname_converter(Reporter._meta.qualified_name, force_schema=True)
+        key_columns = connection.introspection.get_key_columns(cursor, Article._meta.qualified_name)
+        self.assertEqual(key_columns, [(u'reporter_id', rep_tbl, u'id')])
 
     def test_get_primary_key_column(self):
         cursor = connection.cursor()
-        primary_key_column = connection.introspection.get_primary_key_column(cursor, Article._meta.db_table)
+        primary_key_column = connection.introspection.get_primary_key_column(cursor, Article._meta.qualified_name)
         self.assertEqual(primary_key_column, u'id')
 
     def test_get_indexes(self):
         cursor = connection.cursor()
-        indexes = connection.introspection.get_indexes(cursor, Article._meta.db_table)
+        tbl = connection.introspection.qname_converter(Article._meta.qualified_name)
+        indexes = connection.introspection.get_indexes(cursor, tbl)
         self.assertEqual(indexes['reporter_id'], {'unique': False, 'primary_key': False})
 
 
Index: docs/topics/db/models.txt
===================================================================
--- docs/topics/db/models.txt	(revision 17914)
+++ docs/topics/db/models.txt	(working copy)
@@ -636,7 +636,8 @@
             verbose_name_plural = "oxen"
 
 Model metadata is "anything that's not a field", such as ordering options
-(:attr:`~Options.ordering`), database table name (:attr:`~Options.db_table`), or
+(:attr:`~Options.ordering`), database table name (:attr:`~Options.db_table`),
+or custom schema for the tables (:attr:`~Options.db_schema`), or
 human-readable singular and plural names (:attr:`~Options.verbose_name` and
 :attr:`~Options.verbose_name_plural`). None are required, and adding ``class
 Meta`` to a model is completely optional.
Index: docs/ref/models/options.txt
===================================================================
--- docs/ref/models/options.txt	(revision 17914)
+++ docs/ref/models/options.txt	(working copy)
@@ -67,7 +67,23 @@
     the table name via ``db_table``, particularly if you are using the MySQL
     backend. See the :ref:`MySQL notes <mysql-notes>` for more details.
 
+.. _db_schema:
 
+``db_schema``
+-------------
+
+.. attribute:: Options.db_schema
+
+.. versionadded:: 1.5
+
+The name of the database schema to use for the model. If the backend
+doesn't support multiple schemas, this option is ignored.
+
+If this is used then Django will place the table in the given schema.
+Usually this means turning the pair db_schema, db_table into a fully
+qualified name. For example: "db_schema"."db_table".
+
+
 ``db_tablespace``
 -----------------
 
Index: docs/ref/settings.txt
===================================================================
--- docs/ref/settings.txt	(revision 17914)
+++ docs/ref/settings.txt	(working copy)
@@ -512,6 +512,23 @@
 
 The username to use when connecting to the database. Not used with SQLite.
 
+.. setting:: SCHEMA
+
+SCHEMA
+~~~~~~
+
+.. versionadded:: 1.5
+
+Default: ``''`` (Empty string)
+
+The name of the database schema to use for models. If the backend
+doesn't support multiple schemas, this option is ignored. An empty
+string means that schema qualified table names are not used by default.
+
+If this is used then Django will prefix any table names with the schema name.
+The schema can be overriden on a per-model basis, for details see
+:ref:`db_schema`.
+
 .. setting:: TEST_CHARSET
 
 TEST_CHARSET
