Ticket #3163: create_db_schema-trunk7027-withtests.2.diff

File create_db_schema-trunk7027-withtests.2.diff, 24.7 KB (added by honeyman, 7 years ago)

create_db_schema Mera option implementation (svn diff, Django trunk 7027, tests included)

  • django/db/models/options.py

     
    1515
    1616DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
    1717                 'unique_together', 'permissions', 'get_latest_by',
    18                  'order_with_respect_to', 'app_label', 'db_tablespace')
     18                 'order_with_respect_to', 'app_label', 'db_tablespace', 'create_db_schema')
    1919
    2020class Options(object):
    2121    def __init__(self, meta):
     
    3030        self.get_latest_by = None
    3131        self.order_with_respect_to = None
    3232        self.db_tablespace = settings.DEFAULT_TABLESPACE
     33        self.create_db_schema = True
    3334        self.admin = None
    3435        self.meta = meta
    3536        self.pk = None
  • django/core/management/commands/syncdb.py

     
    5757            app_name = app.__name__.split('.')[-2]
    5858            model_list = models.get_models(app)
    5959            for model in model_list:
    60                 # Create the model's database table, if it doesn't already exist.
    61                 if verbosity >= 2:
    62                     print "Processing %s.%s model" % (app_name, model._meta.object_name)
    63                 if table_name_converter(model._meta.db_table) in tables:
    64                     continue
    65                 sql, references = sql_model_create(model, self.style, seen_models)
    66                 seen_models.add(model)
    67                 created_models.add(model)
    68                 for refto, refs in references.items():
    69                     pending_references.setdefault(refto, []).extend(refs)
    70                 sql.extend(sql_for_pending_references(model, self.style, pending_references))
    71                 if verbosity >= 1:
    72                     print "Creating table %s" % model._meta.db_table
    73                 for statement in sql:
    74                     cursor.execute(statement)
    75                 tables.append(table_name_converter(model._meta.db_table))
     60                if model._meta.create_db_schema:
     61                    # Create the model's database table, if it doesn't already exist.
     62                    if verbosity >= 2:
     63                        print "Processing %s.%s model" % (app_name, model._meta.object_name)
     64                    if table_name_converter(model._meta.db_table) in tables:
     65                        continue
     66                    sql, references = sql_model_create(model, self.style, seen_models)
     67                    seen_models.add(model)
     68                    created_models.add(model)
     69                    for refto, refs in references.items():
     70                        pending_references.setdefault(refto, []).extend(refs)
     71                    sql.extend(sql_for_pending_references(model, self.style, pending_references))
     72                    if verbosity >= 1:
     73                        print "Creating table %s" % model._meta.db_table
     74                    for statement in sql:
     75                        cursor.execute(statement)
     76                    tables.append(table_name_converter(model._meta.db_table))
    7677
    7778        # Create the m2m tables. This must be done after all tables have been created
    7879        # to ensure that all referred tables will exist.
     
    119120            app_name = app.__name__.split('.')[-2]
    120121            for model in models.get_models(app):
    121122                if model in created_models:
    122                     index_sql = sql_indexes_for_model(model, self.style)
    123                     if index_sql:
    124                         if verbosity >= 1:
    125                             print "Installing index for %s.%s model" % (app_name, model._meta.object_name)
    126                         try:
    127                             for sql in index_sql:
    128                                 cursor.execute(sql)
    129                         except Exception, e:
    130                             sys.stderr.write("Failed to install index for %s.%s model: %s" % \
    131                                                 (app_name, model._meta.object_name, e))
    132                             transaction.rollback_unless_managed()
    133                         else:
    134                             transaction.commit_unless_managed()
     123                    if model._meta.create_db_schema:
     124                        index_sql = sql_indexes_for_model(model, self.style)
     125                        if index_sql:
     126                            if verbosity >= 1:
     127                                print "Installing index for %s.%s model" % (app_name, model._meta.object_name)
     128                            try:
     129                                for sql in index_sql:
     130                                    cursor.execute(sql)
     131                            except Exception, e:
     132                                sys.stderr.write("Failed to install index for %s.%s model: %s" % \
     133                                                    (app_name, model._meta.object_name, e))
     134                                transaction.rollback_unless_managed()
     135                            else:
     136                                transaction.commit_unless_managed()
    135137
    136138        # Install the 'initial_data' fixture, using format discovery
    137139        from django.core.management import call_command
  • django/core/management/sql.py

     
    1313    cursor = connection.cursor()
    1414    return get_introspection_module().get_table_list(cursor)
    1515
    16 def django_table_list(only_existing=False):
     16def django_table_list(only_existing=False, filter_not_generated_tables=False):
    1717    """
    1818    Returns a list of all table names that have associated Django models and
    1919    are in INSTALLED_APPS.
    2020
    2121    If only_existing is True, the resulting list will only include the tables
    2222    that actually exist in the database.
     23
     24    If filter_not_generated_tables is True, then all tables with associated Django models
     25    which have Meta option create_db_schema=False will not be added to the list.
    2326    """
    2427    from django.db import models
    2528    tables = []
    2629    for app in models.get_apps():
    2730        for model in models.get_models(app):
    28             tables.append(model._meta.db_table)
    29             tables.extend([f.m2m_db_table() for f in model._meta.many_to_many])
     31            if (not filter_not_generated_tables) or (model._meta.create_db_schema):
     32                tables.append(model._meta.db_table)
     33                tables.extend([f.m2m_db_table() for f in model._meta.many_to_many])
    3034    if only_existing:
    3135        existing = table_list()
    3236        tables = [t for t in tables if t in existing]
     
    4549        converter = lambda x: x
    4650    return set([m for m in all_models if converter(m._meta.db_table) in map(converter, table_list)])
    4751
    48 def sequence_list():
    49     "Returns a list of information about all DB sequences for all models in all apps."
     52def sequence_list(filter_not_generated_tables=False):
     53    """
     54    Returns a list of information about all DB sequences for all models in all apps.
     55
     56    If filter_not_generated_tables is True, then only the sequences for the Django models
     57    which have Meta option create_db_schema=False will be added to the list.
     58    """
    5059    from django.db import models
    5160
    5261    apps = models.get_apps()
     
    5463
    5564    for app in apps:
    5665        for model in models.get_models(app):
    57             for f in model._meta.fields:
    58                 if isinstance(f, models.AutoField):
    59                     sequence_list.append({'table': model._meta.db_table, 'column': f.column})
    60                     break # Only one AutoField is allowed per model, so don't bother continuing.
     66            if (not filter_not_generated_tables) or (model._meta.create_db_schema):
     67                for f in model._meta.fields:
     68                    if isinstance(f, models.AutoField):
     69                        sequence_list.append({'table': model._meta.db_table, 'column': f.column})
     70                        break # Only one AutoField is allowed per model, so don't bother continuing.
    6171
    6272            for f in model._meta.many_to_many:
    6373                sequence_list.append({'table': f.m2m_db_table(), 'column': None})
     
    156166    for model in app_models:
    157167        if cursor and table_name_converter(model._meta.db_table) in table_names:
    158168            # Drop the table now
    159             output.append('%s %s;' % (style.SQL_KEYWORD('DROP TABLE'),
    160                 style.SQL_TABLE(qn(model._meta.db_table))))
     169            if model._meta.create_db_schema:
     170                output.append('%s %s;' % (style.SQL_KEYWORD('DROP TABLE'),
     171                    style.SQL_TABLE(qn(model._meta.db_table))))
    161172            if connection.features.supports_constraints and model in references_to_delete:
    162173                for rel_class, f in references_to_delete[model]:
    163174                    table = rel_class._meta.db_table
     
    165176                    r_table = model._meta.db_table
    166177                    r_col = model._meta.get_field(f.rel.field_name).column
    167178                    r_name = '%s_refs_%s_%x' % (col, r_col, abs(hash((table, r_table))))
    168                     output.append('%s %s %s %s;' % \
    169                         (style.SQL_KEYWORD('ALTER TABLE'),
    170                         style.SQL_TABLE(qn(table)),
    171                         style.SQL_KEYWORD(connection.ops.drop_foreignkey_sql()),
    172                         style.SQL_FIELD(truncate_name(r_name, connection.ops.max_name_length()))))
     179                    if rel_class._meta.create_db_schema:
     180                        output.append('%s %s %s %s;' % \
     181                            (style.SQL_KEYWORD('ALTER TABLE'),
     182                            style.SQL_TABLE(qn(table)),
     183                            style.SQL_KEYWORD(connection.ops.drop_foreignkey_sql()),
     184                            style.SQL_FIELD(truncate_name(r_name, connection.ops.max_name_length()))))
    173185                del references_to_delete[model]
    174186            if model._meta.has_auto_field:
    175187                ds = connection.ops.drop_sequence_sql(model._meta.db_table)
     
    206218def sql_flush(style, only_django=False):
    207219    """
    208220    Returns a list of the SQL statements used to flush the database.
    209    
     221
    210222    If only_django is True, then only table names that have associated Django
    211223    models and are in INSTALLED_APPS will be included.
    212224    """
    213225    from django.db import connection
    214226    if only_django:
    215         tables = django_table_list()
     227        tables = django_table_list(filter_not_generated_tables=True)
    216228    else:
    217229        tables = table_list()
    218     statements = connection.ops.sql_flush(style, tables, sequence_list())
     230    statements = connection.ops.sql_flush(style, tables, sequence_list(filter_not_generated_tables=True))
    219231    return statements
    220232
    221233def sql_custom(app):
     
    295307        table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \
    296308            ", ".join([style.SQL_FIELD(qn(opts.get_field(f).column)) for f in field_constraints]))
    297309
    298     full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' (']
    299     for i, line in enumerate(table_output): # Combine and add commas.
    300         full_statement.append('    %s%s' % (line, i < len(table_output)-1 and ',' or ''))
    301     full_statement.append(')')
    302     if opts.db_tablespace and connection.features.supports_tablespaces:
    303         full_statement.append(connection.ops.tablespace_sql(opts.db_tablespace))
    304     full_statement.append(';')
    305     final_output.append('\n'.join(full_statement))
     310    # Now build up the CREATE TABLE section but only if the model requires it
     311    if opts.create_db_schema:
     312        full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' (']
     313        for i, line in enumerate(table_output): # Combine and add commas.
     314            full_statement.append('    %s%s' % (line, i < len(table_output)-1 and ',' or ''))
     315        full_statement.append(')')
     316        if opts.db_tablespace and connection.features.supports_tablespaces:
     317            full_statement.append(connection.ops.tablespace_sql(opts.db_tablespace))
     318        full_statement.append(';')
     319        final_output.append('\n'.join(full_statement))
    306320
    307     if opts.has_auto_field:
    308         # Add any extra SQL needed to support auto-incrementing primary keys.
    309         auto_column = opts.auto_field.db_column or opts.auto_field.name
    310         autoinc_sql = connection.ops.autoinc_sql(opts.db_table, auto_column)
    311         if autoinc_sql:
    312             for stmt in autoinc_sql:
    313                 final_output.append(stmt)
     321        if opts.has_auto_field:
     322            # Add any extra SQL needed to support auto-incrementing primary keys.
     323            auto_column = opts.auto_field.db_column or opts.auto_field.name
     324            autoinc_sql = connection.ops.autoinc_sql(opts.db_table, auto_column)
     325            if autoinc_sql:
     326                for stmt in autoinc_sql:
     327                    final_output.append(stmt)
    314328
    315329    return final_output, pending_references
    316330
     
    335349                # For MySQL, r_name must be unique in the first 64 characters.
    336350                # So we are careful with character usage here.
    337351                r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table))))
    338                 final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
    339                     (qn(r_table), truncate_name(r_name, connection.ops.max_name_length()),
    340                     qn(r_col), qn(table), qn(col),
    341                     connection.ops.deferrable_sql()))
     352                if rel_opts.create_db_schema:
     353                    final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
     354                        (qn(r_table), truncate_name(r_name, connection.ops.max_name_length()),
     355                        qn(r_col), qn(table), qn(col),
     356                        connection.ops.deferrable_sql()))
    342357            del pending_references[model]
    343358    return final_output
    344359
     
    411426            for r_table, r_col, table, col in deferred:
    412427                r_name = '%s_refs_%s_%x' % (r_col, col,
    413428                        abs(hash((r_table, table))))
    414                 final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % 
     429                final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' %
    415430                (qn(r_table),
    416431                truncate_name(r_name, connection.ops.max_name_length()),
    417432                qn(r_col), qn(table), qn(col),
     
    458473    output = []
    459474
    460475    qn = connection.ops.quote_name
    461     for f in model._meta.fields:
    462         if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys):
    463             unique = f.unique and 'UNIQUE ' or ''
    464             tablespace = f.db_tablespace or model._meta.db_tablespace
    465             if tablespace and connection.features.supports_tablespaces:
    466                 tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace)
    467             else:
    468                 tablespace_sql = ''
    469             output.append(
    470                 style.SQL_KEYWORD('CREATE %sINDEX' % unique) + ' ' + \
    471                 style.SQL_TABLE(qn('%s_%s' % (model._meta.db_table, f.column))) + ' ' + \
    472                 style.SQL_KEYWORD('ON') + ' ' + \
    473                 style.SQL_TABLE(qn(model._meta.db_table)) + ' ' + \
    474                 "(%s)" % style.SQL_FIELD(qn(f.column)) + \
    475                 "%s;" % tablespace_sql
    476             )
     476    if model._meta.create_db_schema:
     477        for f in model._meta.fields:
     478            if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys):
     479                unique = f.unique and 'UNIQUE ' or ''
     480                tablespace = f.db_tablespace or model._meta.db_tablespace
     481                if tablespace and connection.features.supports_tablespaces:
     482                    tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace)
     483                else:
     484                    tablespace_sql = ''
     485                output.append(
     486                    style.SQL_KEYWORD('CREATE %sINDEX' % unique) + ' ' + \
     487                    style.SQL_TABLE(qn('%s_%s' % (model._meta.db_table, f.column))) + ' ' + \
     488                    style.SQL_KEYWORD('ON') + ' ' + \
     489                    style.SQL_TABLE(qn(model._meta.db_table)) + ' ' + \
     490                    "(%s)" % style.SQL_FIELD(qn(f.column)) + \
     491                    "%s;" % tablespace_sql
     492                )
    477493    return output
    478494
    479495def emit_post_sync_signal(created_models, verbosity, interactive):
  • tests/modeltests/create_db_schema/models.py

     
     1"""
     2xx. create_db_schema
     3
     4Models can have a ``create_db_schema`` attribute, which specifies
     5whether the SQL code is generated for the table on various manage.py operations
     6or not.
     7"""
     8
     9from django.db import models
     10
     11"""
     12General test strategy:
     13* All tests are numbered (01, 02, 03... etc).
     14* Each test contains three models (A, B, C, followed with the number of test),
     15  containing both indexed and non-indexed fields (to verify sql_index),
     16  usual fields (model A), foreign keys (model B) and many-to-many fields (model C).
     17  D table is generated automatically as intermediate M2M one.
     18* The normal (default; create_db_schema = True) behaviour during the manage.py
     19  operations is not thoroughly checked; it is the duty of the appropriate tests
     20  for the primary functionality of these operations.
     21  The most attention is paid to whether the create_db_schema = False
     22  disables the SQL generation properly.
     23* The intermediate table for M2M relations is not ever verified explicitly,
     24  because it is not ever marked with create_db_schema explicitly.
     25"""
     26
     27# This dictionary maps the name of the model/SQL table (like 'A01')
     28# to the boolean specifying whether this name should appear in the final SQL
     29checks = {}
     30
     31
     32"""
     3301: create_db_schema is not set.
     34    In such case, it should be equal (by default) to True,
     35    and SQL is generated for all three models.
     36"""
     37checks['A01'] = True
     38checks['B01'] = True
     39checks['C01'] = True
     40
     41class A01(models.Model):
     42    class Meta: db_table = 'A01'
     43
     44    f_a = models.TextField(db_index = True)
     45    f_b = models.IntegerField()
     46
     47class B01(models.Model):
     48    class Meta: db_table = 'B01'
     49
     50    fk_a = models.ForeignKey(A01)
     51    f_a = models.TextField(db_index = True)
     52    f_b = models.IntegerField()
     53
     54class C01(models.Model):
     55    class Meta: db_table = 'C01'
     56
     57    mm_a = models.ManyToManyField(A01, db_table = 'D01')
     58    f_a = models.TextField(db_index = True)
     59    f_b = models.IntegerField()
     60
     61
     62"""
     6302: create_db_schema is set to True.
     64    SQL is generated for all three models.
     65"""
     66checks['A02'] = True
     67checks['B02'] = True
     68checks['C02'] = True
     69
     70class A02(models.Model):
     71    class Meta:
     72        db_table = 'A02'
     73        create_db_schema = True
     74
     75    f_a = models.TextField(db_index = True)
     76    f_b = models.IntegerField()
     77
     78class B02(models.Model):
     79    class Meta:
     80        db_table = 'B02'
     81        create_db_schema = True
     82
     83    fk_a = models.ForeignKey(A02)
     84    f_a = models.TextField(db_index = True)
     85    f_b = models.IntegerField()
     86
     87class C02(models.Model):
     88    class Meta:
     89        db_table = 'C02'
     90        create_db_schema = True
     91
     92    mm_a = models.ManyToManyField(A02, db_table = 'D02')
     93    f_a = models.TextField(db_index = True)
     94    f_b = models.IntegerField()
     95
     96
     97"""
     9803: create_db_schema is set to False.
     99    SQL is NOT generated for any of the three models.
     100"""
     101checks['A03'] = False
     102checks['B03'] = False
     103checks['C03'] = False
     104
     105class A03(models.Model):
     106    class Meta:
     107        db_table = 'A03'
     108        create_db_schema = False
     109
     110    f_a = models.TextField(db_index = True)
     111    f_b = models.IntegerField()
     112
     113class B03(models.Model):
     114    class Meta:
     115        db_table = 'B03'
     116        create_db_schema = False
     117
     118    fk_a = models.ForeignKey(A03)
     119    f_a = models.TextField(db_index = True)
     120    f_b = models.IntegerField()
     121
     122class C03(models.Model):
     123    class Meta:
     124        db_table = 'C03'
     125        create_db_schema = False
     126
     127    mm_a = models.ManyToManyField(A03, db_table = 'D03')
     128    f_a = models.TextField(db_index = True)
     129    f_b = models.IntegerField()
     130
     131
     132# We will use short names for these templates
     133sql_templates = {
     134        'create table': 'CREATE TABLE "%s"',
     135        'create index': 'CREATE INDEX "%s_f_a"',
     136        'drop table': 'DROP TABLE "%s"',
     137        'delete from': 'DELETE FROM "%s"'
     138}
     139
     140def get_failed_models(arr_sql, sql_template_names):
     141    """
     142    Find the models which should not be in the SQL but they are present,
     143    or they should be in the SQL but they are missing.
     144    """
     145    txt_sql = ' '.join(arr_sql)
     146    for (model, should_be_present) in checks.iteritems():
     147        # Do we expect to see the model name in the SQL text?
     148        for sql_template_name in sql_template_names:
     149            # We are interested not in just the model name like "A01",
     150            # but in the whole string like 'CREATE TABLE "A01"'
     151            # so we apply the model name to the template
     152            # to find out the expected string
     153            expected = (sql_templates[sql_template_name])%model
     154            if ((expected in txt_sql) != should_be_present):
     155                # Our expectations failed!
     156                yield 'The string %s %s present in SQL but it %s.'%(
     157                    expected,
     158                    {False: 'is not', True: 'is'}[expected in txt_sql],
     159                    {False: 'should not be', True: 'should be'}[should_be_present]
     160                    )
     161
     162
     163__test__ = {'API_TESTS':"""
     164>>> from django.db.models import get_app
     165>>> from django.core.management.sql import *
     166>>> from django.core.management.color import no_style
     167>>> import sys
     168
     169>>> myapp = get_app('create_db_schema')
     170>>> mystyle = no_style()
     171
     172# a. Verify sql_create
     173>>> list(get_failed_models( sql_create(myapp, mystyle), ['create table'] ))
     174[]
     175
     176# b. Verify sql_delete
     177>>> list(get_failed_models( sql_delete(myapp, mystyle), ['drop table'] ))
     178[]
     179
     180# c. Verify sql_reset
     181>>> list(get_failed_models( sql_reset(myapp, mystyle), ['drop table', 'create table', 'create index'] ))
     182[]
     183
     184# d. Verify sql_flush
     185>>> # sql_flush(mystyle)
     186>>> list(get_failed_models( sql_flush(mystyle), ['delete from'] ))
     187[]
     188
     189# e. Verify sql_custom
     190# No custom data provided, should not be no output.
     191>>> sql_custom(myapp)
     192[]
     193
     194# f. Verify sql_indexes
     195>>> list(get_failed_models( sql_indexes(myapp, mystyle), ['create index'] ))
     196[]
     197
     198# g. Verify sql_all
     199>>> list(get_failed_models( sql_all(myapp, mystyle), ['create table', 'create index'] ))
     200[]
     201"""}
  • AUTHORS

     
    353353    ymasuda@ethercube.com
    354354    Jarek Zgoda <jarek.zgoda@gmail.com>
    355355    Cheng Zhang
     356    Alexander Myodov <amyodov@gmail.com>
    356357
    357358A big THANK YOU goes to:
    358359
  • docs/model-api.txt

     
    10511051that aren't allowed in Python variable names -- notably, the hyphen --
    10521052that's OK. Django quotes column and table names behind the scenes.
    10531053
     1054``create_db_schema``
     1055--------------------
     1056
     1057**New in Django development version**
     1058
     1059Marks this model as requiring SQL operations when calling ``manage.py``::
     1060
     1061    create_db_schema = False
     1062
     1063If this isn't given, Django will use ``create_db_schema = True``
     1064what means that the operations like ``manage.py sqlreset``, ``manage.py syncdb``
     1065and others will regenerate the appropriate table when needed.
     1066If the option is set to False, the appropriate table will not be affected with
     1067any SQL operations.
     1068
     1069This is useful for databases where some DB tables are controlled by Django models,
     1070but some other DB tables and views are created via raw SQL and should not be affected
     1071by any ``manage.py`` actions.
     1072Note that if the initial SQL data is provided (see `Providing initial SQL
     1073data`_ below), it still will be present in the output of
     1074``sqlall``/``sqlcustom`` commands.
     1075
    10541076``db_tablespace``
    10551077-----------------
    10561078
Back to Top