Ticket #6148: generic-db_schema-r8696.diff

File generic-db_schema-r8696.diff, 21.0 KB (added by crippledcanary, 7 years ago)

Works with postgres and mysql but testing only works on postgres due to mysqls schema implementation.

  • django/core/management/commands/syncdb.py

     
    6161            app_name = app.__name__.split('.')[-2]
    6262            model_list = models.get_models(app)
    6363            for model in model_list:
     64                # Add model defined schema tables if anny
     65                if model._meta.db_schema:
     66                    tables += connection.introspection.schema_table_names(model._meta.db_schema)
    6467                # Create the model's database table, if it doesn't already exist.
    6568                if verbosity >= 2:
    6669                    print "Processing %s.%s model" % (app_name, model._meta.object_name)
  • django/core/management/sql.py

     
    8484    references_to_delete = {}
    8585    app_models = models.get_models(app)
    8686    for model in app_models:
     87        schema = model._meta.db_schema
     88        # Find aditional tables in model defined schemas
     89        if schema:
     90            table_names += connection.introspection.get_schema_table_list(cursor, schema)
    8791        if cursor and connection.introspection.table_name_converter(model._meta.db_table) in table_names:
    8892            # The table exists, so it needs to be dropped
    8993            opts = model._meta
  • django/db/backends/__init__.py

     
    225225        """
    226226        raise NotImplementedError()
    227227
     228    def prep_db_table(self, db_schema, db_table):
     229        """
     230        Prepares and formats the table name if neccesary.
     231        Just returns the db_table if not supported
     232        """
     233        return db_table
     234
    228235    def random_function_sql(self):
    229236        """
    230237        Returns a SQL expression that returns a random value.
     
    386393        cursor = self.connection.cursor()
    387394        return self.get_table_list(cursor)
    388395
     396    def schema_name_converter(self, name):
     397        """Apply a conversion to the name for the purposes of comparison.
     398
     399        The default schema name converter is for case sensitive comparison.
     400        """
     401        return name
     402
     403    def get_schema_list(self, cursor):
     404        "Returns a list of schemas that exist in the database"
     405        return []
     406   
     407    def get_schema_table_list(self, cursor, schema):
     408        "Returns a list of tables in a specific schema"
     409        return []
     410       
     411    def schema_names(self):
     412        cursor = self.connection.cursor()
     413        return self.get_schema_list(cursor)
     414   
     415    def schema_table_names(self, schema):
     416        "Returns a list of names of all tables that exist in the database schema."
     417        cursor = self.connection.cursor()
     418        return self.get_schema_table_list(cursor, schema)
     419
    389420    def django_table_names(self, only_existing=False):
    390421        """
    391422        Returns a list of all table names that have associated Django models and
  • django/db/backends/creation.py

     
    2525    def __init__(self, connection):
    2626        self.connection = connection
    2727
     28    def default_schema(self):
     29        return ""
     30
     31    def sql_create_schema(self, schema, style):
     32        """"
     33        Returns the SQL required to create a single schema
     34        """
     35        qn = self.connection.ops.quote_name
     36        output = "%s %s;" % (style.SQL_KEYWORD('CREATE SCHEMA'), qn(schema))
     37        return output
     38
    2839    def sql_create_model(self, model, style, known_models=set()):
    2940        """
    3041        Returns the SQL required to create a single model, as a tuple of:
     
    122133                r_col = f.column
    123134                table = opts.db_table
    124135                col = opts.get_field(f.rel.field_name).column
     136                # Add schema if we are related to a model in different schema
     137                # and we are not in a different schema ourselfs
     138                if rel_opts.db_schema and not opts.db_schema:
     139                    table =  "%s.%s" % (self.default_schema(), table)
    125140                # For MySQL, r_name must be unique in the first 64 characters.
    126141                # So we are careful with character usage here.
    127142                r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table))))
     
    243258                    tablespace_sql = ''
    244259            else:
    245260                tablespace_sql = ''
     261            # Use original db_table in index name if schema is provided
     262            if model._meta.db_schema:
     263                index_table_name = model._meta._db_table
     264            else:
     265                index_table_name = model._meta.db_table
    246266            output = [style.SQL_KEYWORD('CREATE INDEX') + ' ' +
    247                 style.SQL_TABLE(qn('%s_%s' % (model._meta.db_table, f.column))) + ' ' +
     267                style.SQL_TABLE(qn('%s_%s' % (index_table_name, f.column))) + ' ' +
    248268                style.SQL_KEYWORD('ON') + ' ' +
    249269                style.SQL_TABLE(qn(model._meta.db_table)) + ' ' +
    250270                "(%s)" % style.SQL_FIELD(qn(f.column)) +
     
    253273            output = []
    254274        return output
    255275
     276    def sql_destroy_schema(self, schema, style):
     277        """"
     278        Returns the SQL required to create a single schema
     279        """
     280        qn = self.connection.ops.quote_name
     281        output = "%s %s CASCADE;" % (style.SQL_KEYWORD('DROP SCHEMA IF EXISTS'), qn(schema))
     282        return output
     283
    256284    def sql_destroy_model(self, model, references_to_delete, style):
    257285        "Return the DROP TABLE and restraint dropping statements for a single model"
    258286        # Drop the table now
     
    324352
    325353        return test_database_name
    326354
     355    def _create_test_schemas(self, verbosity, schemas, cursor):
     356        from django.core.management.color import color_style
     357        style = color_style()
     358        for schema in schemas:
     359            if verbosity >= 1:
     360                print "Creating schema %s" % schema
     361            cursor.execute(self.sql_create_schema(schema, style))
     362
     363    def _destroy_test_schemas(self, verbosity, schemas, cursor):
     364        from django.core.management.color import color_style
     365        style = color_style()
     366        for schema in schemas:
     367            if verbosity >= 1:
     368                print "Destroying schema %s" % schema
     369                cursor.execute(self.sql_destroy_schema(schema, style))
     370            if verbosity >= 1:
     371                print "Schema %s destroyed" % schema
     372
     373    def _get_schemas(self, apps):
     374        from django.db import models
     375        schemas = set()
     376        for app in apps:
     377            app_models = models.get_models(app)
     378            for model in app_models:
     379                schema = model._meta.db_schema
     380                if not schema or schema in schemas:
     381                    continue
     382                schemas.add(schema)
     383        return schemas
     384
     385    def _get_app_with_schemas(self):
     386        from django.db import models
     387        apps = models.get_apps()
     388        schema_apps = set()
     389        for app in apps:
     390            app_models = models.get_models(app)
     391            for model in app_models:
     392                schema = model._meta.db_schema
     393                if not schema or app in schema_apps:
     394                    continue
     395                schema_apps.add(app)
     396                continue
     397               
     398        return schema_apps
     399
    327400    def _create_test_db(self, verbosity, autoclobber):
    328401        "Internal implementation - creates the test db tables."
     402        schema_apps = self._get_app_with_schemas()
     403        schemas = self._get_schemas(schema_apps)
    329404        suffix = self.sql_table_creation_suffix()
    330405
    331406        if settings.TEST_DATABASE_NAME:
     
    342417        self.set_autocommit()
    343418        try:
    344419            cursor.execute("CREATE DATABASE %s %s" % (qn(test_database_name), suffix))
     420            #Connect to the new database to create schemas in it
     421            self.connection.close()
     422            settings.DATABASE_NAME = test_database_name
     423            cursor = self.connection.cursor()
     424            self.set_autocommit()
     425            self._create_test_schemas(verbosity, schemas, cursor)
    345426        except Exception, e:
    346427            sys.stderr.write("Got an error creating the test database: %s\n" % e)
    347428            if not autoclobber:
     
    350431                try:
    351432                    if verbosity >= 1:
    352433                        print "Destroying old test database..."
     434                    self._destroy_test_schemas(verbosity, schemas, cursor)
    353435                    cursor.execute("DROP DATABASE %s" % qn(test_database_name))
    354436                    if verbosity >= 1:
    355437                        print "Creating test database..."
    356438                    cursor.execute("CREATE DATABASE %s %s" % (qn(test_database_name), suffix))
     439                    #Connect to the new database to create schemas in it
     440                    self.connection.close()
     441                    settings.DATABASE_NAME = test_database_name
     442                    cursor = self.connection.cursor()
     443                    self.set_autocommit()
     444                    self._create_test_schemas(verbosity, schemas, cursor)
    357445                except Exception, e:
    358446                    sys.stderr.write("Got an error recreating the test database: %s\n" % e)
    359447                    sys.exit(2)
  • django/db/backends/mysql/base.py

     
    142142    def quote_name(self, name):
    143143        if name.startswith("`") and name.endswith("`"):
    144144            return name # Quoting once is enough.
    145         return "`%s`" % name
     145        # add support for tablenames passed that also have their schema in their name
     146        return "`%s`" % name.replace('.','`.`')
    146147
     148    def prep_db_table(self, db_schema, db_table):
     149        return "%s.%s" % (db_schema, db_table)
     150
    147151    def random_function_sql(self):
    148152        return 'RAND()'
    149153
  • django/db/backends/mysql/creation.py

     
    6565                field.rel.to._meta.db_table, field.rel.to._meta.pk.column)
    6666            ]
    6767        return table_output, deferred
    68        
    69  No newline at end of file
     68
     69    def default_schema(self):
     70        return settings.DATABASE_NAME
     71
     72    def sql_create_schema(self, schema, style):
     73        """
     74        Returns the SQL required to create a single schema.
     75        In MySQL schemas are synonymous to databases
     76        """
     77        qn = self.connection.ops.quote_name
     78        output = "%s %s;" % (style.SQL_KEYWORD('CREATE DATABASE'), qn(schema))
     79        return output
     80
     81    def sql_destroy_schema(self, schema, style):
     82        """"
     83        Returns the SQL required to create a single schema
     84        """
     85        qn = self.connection.ops.quote_name
     86        output = "%s %s;" % (style.SQL_KEYWORD('DROP DATABASE IF EXISTS'), qn(schema))
     87        return output
     88 No newline at end of file
  • django/db/backends/mysql/introspection.py

     
    3333        cursor.execute("SHOW TABLES")
    3434        return [row[0] for row in cursor.fetchall()]
    3535
     36    def get_schema_list(self, cursor):
     37        cursor.execute("SHOW SCHEMAS")
     38        return [row[0] for row in cursor.fetchall()]
     39
     40    def get_schema_table_list(self, cursor, schema):
     41        cursor.execute("SHOW TABLES FROM %s" % self.connection.ops.quote_name(schema))
     42        return [schema + "." + row[0] for row in cursor.fetchall()]
     43
    3644    def get_table_description(self, cursor, table_name):
    3745        "Returns a description of the table, with the DB-API cursor.description interface."
    3846        cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name))
  • django/db/backends/postgresql/introspection.py

     
    2929                AND pg_catalog.pg_table_is_visible(c.oid)""")
    3030        return [row[0] for row in cursor.fetchall()]
    3131
     32    def get_schema_list(self, cursor):
     33        cursor.execute("""
     34            SELECT DISTINCT n.nspname
     35            FROM pg_catalog.pg_class c
     36            LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
     37            WHERE c.relkind IN ('r', 'v', '')
     38            AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema')""")   
     39        return [row[0] for row in cursor.fetchall()]
     40
     41    def get_schema_table_list(self, cursor, schema):
     42        cursor.execute("""
     43            SELECT c.relname
     44            FROM pg_catalog.pg_class c
     45            LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
     46            WHERE c.relkind IN ('r', 'v', '')
     47                AND n.nspname = '%s'""" % schema)
     48        return [schema + "." + row[0] for row in cursor.fetchall()]
     49
    3250    def get_table_description(self, cursor, table_name):
    3351        "Returns a description of the table, with the DB-API cursor.description interface."
    3452        cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name))
  • django/db/backends/postgresql/operations.py

     
    5555        return '%s'
    5656
    5757    def last_insert_id(self, cursor, table_name, pk_name):
    58         cursor.execute("SELECT CURRVAL('\"%s_%s_seq\"')" % (table_name, pk_name))
     58        # add support for tablenames passed that also have their schema in their name
     59        cursor.execute(("SELECT CURRVAL('\"%s_%s_seq\"')" % (table_name, pk_name)).replace('.', '"."'))
    5960        return cursor.fetchone()[0]
    6061
    6162    def no_limit_value(self):
     
    6465    def quote_name(self, name):
    6566        if name.startswith('"') and name.endswith('"'):
    6667            return name # Quoting once is enough.
    67         return '"%s"' % name
     68        # add support for tablenames passed that also have their schema in their name
     69        return '"%s"' % name.replace('.','"."')
    6870
     71    def prep_db_table(self, db_schema, db_table):
     72        return "%s.%s" % (db_schema, db_table)
     73
    6974    def sql_flush(self, style, tables, sequences):
    7075        if tables:
    7176            if self.postgres_version[0] >= 8 and self.postgres_version[1] >= 1:
  • django/db/models/options.py

     
    2121DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
    2222                 'unique_together', 'permissions', 'get_latest_by',
    2323                 'order_with_respect_to', 'app_label', 'db_tablespace',
    24                  'abstract')
     24                 'abstract', 'db_schema')
    2525
    2626class Options(object):
    2727    def __init__(self, meta, app_label=None):
     
    2929        self.module_name, self.verbose_name = None, None
    3030        self.verbose_name_plural = None
    3131        self.db_table = ''
     32        self.db_schema = ''
    3233        self.ordering = []
    3334        self.unique_together =  []
    3435        self.permissions =  []
     
    9596            self.db_table = "%s_%s" % (self.app_label, self.module_name)
    9697            self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
    9798
     99        # Patch db_table with the schema if provided and allowed
     100        if self.db_schema:
     101            # Store original db_table in a save place first
     102            self._db_table = self.db_table
     103            self.db_table = connection.ops.prep_db_table(self.db_schema, self.db_table)
     104            # If no changes were done then backend don't support schemas
     105            if self._db_table == self.db_table:
     106                self.db_schema = ''
    98107
    99108    def _prepare(self, model):
    100109        if self.order_with_respect_to:
  • docs/ref/models/options.txt

     
    4242aren't allowed in Python variable names -- notably, the hyphen -- that's OK.
    4343Django quotes column and table names behind the scenes.
    4444
     45``db_schema``
     46-----------------
     47
     48**New in Django development version**
     49
     50The name of the database schema to use for the model. If the backend
     51doesn't support multiple schemas, this options is ignored.
     52
     53If this is used Django will prefix any table names with the schema name.
     54For example MySQL Django would use ``db_schema + '.' + db_table``.
     55Be aware that postgres supports different schemas within the database.
     56MySQL solves the same thing by treating it as just another database.
     57
     58
     59
    4560``db_tablespace``
    4661-----------------
    4762
  • docs/topics/db/models.txt

     
    594594            verbose_name_plural = "oxen"
    595595
    596596Model metadata is "anything that's not a field", such as ordering options
    597 (:attr:`~Options.ordering`), database table name (:attr:`~Options.db_table`), or
     597(:attr:`~Options.ordering`), database table name (:attr:`~Options.db_table`),
     598(:attr:`~Options.db_schema`) custom schema for the tables, or
    598599human-readable singular and plural names (:attr:`~Options.verbose_name` and
    599600:attr:`~Options.verbose_name_plural`). None are required, and adding ``class
    600601Meta`` to a model is completely optional.
  • tests/modeltests/schemas/__init__.py

     
     1#
  • tests/modeltests/schemas/models.py

     
     1# coding: utf-8
     2
     3from django.db import models
     4
     5
     6class Blog(models.Model):
     7    "Model in default schema"
     8    name = models.CharField(max_length=50)
     9
     10   
     11class Entry(models.Model):
     12    "Model in custom schema that reference the default"
     13    blog = models.ForeignKey(Blog)   
     14    title = models.CharField(max_length=50)
     15   
     16    class Meta:
     17        "using custom db_table as well"
     18        db_table='schema_blog_entries'
     19        db_schema = 'test_schema'
     20       
     21
     22class Comment(models.Model):
     23    "Model in the custom schema that references Entry in the same schema"
     24    entry = models.ForeignKey(Entry)
     25    text = models.CharField(max_length=50)
     26   
     27    class Meta:
     28        db_schema = 'test_schema'
     29
     30__test__ = {'API_TESTS': """
     31
     32#Test with actual data
     33# Nothing in there yet
     34>>> Blog.objects.all()
     35[]
     36
     37# Create a blog
     38>>> b = Blog(name='Test')
     39>>> b.save()
     40
     41# Verify that we got an ID
     42>>> b.id
     431
     44
     45# Create entry
     46>>> e = Entry(blog=b, title='Test entry')
     47>>> e.save()
     48>>> e.id
     491
     50
     51# Create Comments
     52>>> c1 = Comment(entry=e, text='nice entry')
     53>>> c1.save()
     54>>> c2 = Comment(entry=e, text='really like it')
     55>>> c2.save()
     56
     57#Retrieve the stuff again.
     58>>> b2 = Blog.objects.get(id=b.id)
     59>>> b==b2
     60True
     61
     62>>> b2.entry_set.all()
     63[<Entry: Entry object>]
     64
     65>>> from django.conf import settings
     66>>> from django.db import connection, models
     67
     68# Test if we support schemas and can find the table if so
     69>>> if e._meta.db_schema:
     70...     tables = connection.introspection.schema_table_names(e._meta.db_schema)
     71... else:
     72...     tables = connection.introspection.table_names()
     73>>> if connection.introspection.table_name_converter(e._meta.db_table) in tables:
     74...     print "ok"
     75... else:
     76...     print "schema=" + e._meta.db_schema
     77...     print tables
     78ok
     79
     80# Test that all but sqlite3 backend suports schema and doesn't drop it.
     81# Oracle is not tested
     82>>> if settings.DATABASE_ENGINE != 'sqlite3' and settings.DATABASE_ENGINE != 'oracle':
     83...     if e._meta.db_schema != 'test_schema':
     84...         print "shouldn't drop or modify schema"
     85
     86>>> from django.core.management.sql import *
     87>>> from django.core.management.color import color_style
     88
     89>>> style = color_style()
     90>>> app = models.get_app('schemas')
     91
     92# Get the sql_create sequence
     93>>> a = sql_create(app, style)
     94
     95
     96#Done
     97"""
     98}
Back to Top