Ticket #3163: create_db_schema-trunk7442-withtests.diff

File create_db_schema-trunk7442-withtests.diff, 24.9 KB (added by honeyman, 7 years ago)

create_db_schema Mera option implementation (svn diff, Django trunk 7442, 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                     if refto in seen_models:
    71                         sql.extend(sql_for_pending_references(refto, self.style, pending_references))
    72                 sql.extend(sql_for_pending_references(model, self.style, pending_references))
    73                 if verbosity >= 1:
    74                     print "Creating table %s" % model._meta.db_table
    75                 for statement in sql:
    76                     cursor.execute(statement)
    77                 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                        if refto in seen_models:
     72                            sql.extend(sql_for_pending_references(refto, self.style, pending_references))
     73                    sql.extend(sql_for_pending_references(model, self.style, pending_references))
     74                    if verbosity >= 1:
     75                        print "Creating table %s" % model._meta.db_table
     76                    for statement in sql:
     77                        cursor.execute(statement)
     78                    tables.append(table_name_converter(model._meta.db_table))
    7879
    7980        # Create the m2m tables. This must be done after all tables have been created
    8081        # to ensure that all referred tables will exist.
     
    121122            app_name = app.__name__.split('.')[-2]
    122123            for model in models.get_models(app):
    123124                if model in created_models:
    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()
     125                    if model._meta.create_db_schema:
     126                        index_sql = sql_indexes_for_model(model, self.style)
     127                        if index_sql:
     128                            if verbosity >= 1:
     129                                print "Installing index for %s.%s model" % (app_name, model._meta.object_name)
     130                            try:
     131                                for sql in index_sql:
     132                                    cursor.execute(sql)
     133                            except Exception, e:
     134                                sys.stderr.write("Failed to install index for %s.%s model: %s" % \
     135                                                    (app_name, model._meta.object_name, e))
     136                                transaction.rollback_unless_managed()
     137                            else:
     138                                transaction.commit_unless_managed()
    137139
    138140        # Install the 'initial_data' fixture, using format discovery
    139141        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})
     
    158168    for model in app_models:
    159169        if cursor and table_name_converter(model._meta.db_table) in table_names:
    160170            # Drop the table now
    161             output.append('%s %s;' % (style.SQL_KEYWORD('DROP TABLE'),
    162                 style.SQL_TABLE(qn(model._meta.db_table))))
     171            if model._meta.create_db_schema:
     172                output.append('%s %s;' % (style.SQL_KEYWORD('DROP TABLE'),
     173                    style.SQL_TABLE(qn(model._meta.db_table))))
    163174            if connection.features.supports_constraints and model in references_to_delete:
    164175                for rel_class, f in references_to_delete[model]:
    165176                    table = rel_class._meta.db_table
     
    167178                    r_table = model._meta.db_table
    168179                    r_col = model._meta.get_field(f.rel.field_name).column
    169180                    r_name = '%s_refs_%s_%x' % (col, r_col, abs(hash((table, r_table))))
    170                     output.append('%s %s %s %s;' % \
    171                         (style.SQL_KEYWORD('ALTER TABLE'),
    172                         style.SQL_TABLE(qn(table)),
    173                         style.SQL_KEYWORD(connection.ops.drop_foreignkey_sql()),
    174                         style.SQL_FIELD(truncate_name(r_name, connection.ops.max_name_length()))))
     181                    if rel_class._meta.create_db_schema:
     182                        output.append('%s %s %s %s;' % \
     183                            (style.SQL_KEYWORD('ALTER TABLE'),
     184                            style.SQL_TABLE(qn(table)),
     185                            style.SQL_KEYWORD(connection.ops.drop_foreignkey_sql()),
     186                            style.SQL_FIELD(truncate_name(r_name, connection.ops.max_name_length()))))
    175187                del references_to_delete[model]
    176188            if model._meta.has_auto_field:
    177189                ds = connection.ops.drop_sequence_sql(model._meta.db_table)
     
    208220def sql_flush(style, only_django=False):
    209221    """
    210222    Returns a list of the SQL statements used to flush the database.
    211    
     223
    212224    If only_django is True, then only table names that have associated Django
    213225    models and are in INSTALLED_APPS will be included.
    214226    """
    215227    from django.db import connection
    216228    if only_django:
    217         tables = django_table_list()
     229        tables = django_table_list(filter_not_generated_tables=True)
    218230    else:
    219231        tables = table_list()
    220     statements = connection.ops.sql_flush(style, tables, sequence_list())
     232    statements = connection.ops.sql_flush(style, tables, sequence_list(filter_not_generated_tables=True))
    221233    return statements
    222234
    223235def sql_custom(app):
     
    303315                opts.db_tablespace, inline=True))
    304316        table_output.append(' '.join(constraint_output))
    305317
    306     full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' (']
    307     for i, line in enumerate(table_output): # Combine and add commas.
    308         full_statement.append('    %s%s' % (line, i < len(table_output)-1 and ',' or ''))
    309     full_statement.append(')')
    310     if opts.db_tablespace and connection.features.supports_tablespaces:
    311         full_statement.append(connection.ops.tablespace_sql(opts.db_tablespace))
    312     full_statement.append(';')
    313     final_output.append('\n'.join(full_statement))
     318    # Now build up the CREATE TABLE section but only if the model requires it
     319    if opts.create_db_schema:
     320        full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' (']
     321        for i, line in enumerate(table_output): # Combine and add commas.
     322            full_statement.append('    %s%s' % (line, i < len(table_output)-1 and ',' or ''))
     323        full_statement.append(')')
     324        if opts.db_tablespace and connection.features.supports_tablespaces:
     325            full_statement.append(connection.ops.tablespace_sql(opts.db_tablespace))
     326        full_statement.append(';')
     327        final_output.append('\n'.join(full_statement))
    314328
    315     if opts.has_auto_field:
    316         # Add any extra SQL needed to support auto-incrementing primary keys.
    317         auto_column = opts.auto_field.db_column or opts.auto_field.name
    318         autoinc_sql = connection.ops.autoinc_sql(opts.db_table, auto_column)
    319         if autoinc_sql:
    320             for stmt in autoinc_sql:
    321                 final_output.append(stmt)
     329        if opts.has_auto_field:
     330            # Add any extra SQL needed to support auto-incrementing primary keys.
     331            auto_column = opts.auto_field.db_column or opts.auto_field.name
     332            autoinc_sql = connection.ops.autoinc_sql(opts.db_table, auto_column)
     333            if autoinc_sql:
     334                for stmt in autoinc_sql:
     335                    final_output.append(stmt)
    322336
    323337    return final_output, pending_references
    324338
     
    343357                # For MySQL, r_name must be unique in the first 64 characters.
    344358                # So we are careful with character usage here.
    345359                r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table))))
    346                 final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
    347                     (qn(r_table), truncate_name(r_name, connection.ops.max_name_length()),
    348                     qn(r_col), qn(table), qn(col),
    349                     connection.ops.deferrable_sql()))
     360                if rel_opts.create_db_schema:
     361                    final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
     362                        (qn(r_table), truncate_name(r_name, connection.ops.max_name_length()),
     363                        qn(r_col), qn(table), qn(col),
     364                        connection.ops.deferrable_sql()))
    350365            del pending_references[model]
    351366    return final_output
    352367
     
    419434            for r_table, r_col, table, col in deferred:
    420435                r_name = '%s_refs_%s_%x' % (r_col, col,
    421436                        abs(hash((r_table, table))))
    422                 final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % 
     437                final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' %
    423438                (qn(r_table),
    424439                truncate_name(r_name, connection.ops.max_name_length()),
    425440                qn(r_col), qn(table), qn(col),
     
    466481    output = []
    467482
    468483    qn = connection.ops.quote_name
    469     for f in model._meta.fields:
    470         if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys):
    471             unique = f.unique and 'UNIQUE ' or ''
    472             tablespace = f.db_tablespace or model._meta.db_tablespace
    473             if tablespace and connection.features.supports_tablespaces:
    474                 tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace)
    475             else:
    476                 tablespace_sql = ''
    477             output.append(
    478                 style.SQL_KEYWORD('CREATE %sINDEX' % unique) + ' ' + \
    479                 style.SQL_TABLE(qn('%s_%s' % (model._meta.db_table, f.column))) + ' ' + \
    480                 style.SQL_KEYWORD('ON') + ' ' + \
    481                 style.SQL_TABLE(qn(model._meta.db_table)) + ' ' + \
    482                 "(%s)" % style.SQL_FIELD(qn(f.column)) + \
    483                 "%s;" % tablespace_sql
    484             )
     484    if model._meta.create_db_schema:
     485        for f in model._meta.fields:
     486            if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys):
     487                unique = f.unique and 'UNIQUE ' or ''
     488                tablespace = f.db_tablespace or model._meta.db_tablespace
     489                if tablespace and connection.features.supports_tablespaces:
     490                    tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace)
     491                else:
     492                    tablespace_sql = ''
     493                output.append(
     494                    style.SQL_KEYWORD('CREATE %sINDEX' % unique) + ' ' + \
     495                    style.SQL_TABLE(qn('%s_%s' % (model._meta.db_table, f.column))) + ' ' + \
     496                    style.SQL_KEYWORD('ON') + ' ' + \
     497                    style.SQL_TABLE(qn(model._meta.db_table)) + ' ' + \
     498                    "(%s)" % style.SQL_FIELD(qn(f.column)) + \
     499                    "%s;" % tablespace_sql
     500                )
    485501    return output
    486502
    487503def 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

     
    381381    ymasuda@ethercube.com
    382382    Jarek Zgoda <jarek.zgoda@gmail.com>
    383383    Cheng Zhang
     384    Alexander Myodov <amyodov@gmail.com>
    384385
    385386A big THANK YOU goes to:
    386387
  • docs/model-api.txt

     
    10621062that aren't allowed in Python variable names -- notably, the hyphen --
    10631063that's OK. Django quotes column and table names behind the scenes.
    10641064
     1065``create_db_schema``
     1066--------------------
     1067
     1068**New in Django development version**
     1069
     1070Marks this model as requiring SQL operations when calling ``manage.py``::
     1071
     1072    create_db_schema = False
     1073
     1074If this isn't given, Django will use ``create_db_schema = True``
     1075what means that the operations like ``manage.py sqlreset``, ``manage.py syncdb``
     1076and others will regenerate the appropriate table when needed.
     1077If the option is set to False, the appropriate table will not be affected with
     1078any SQL operations.
     1079
     1080This is useful for databases where some DB tables are controlled by Django models,
     1081but some other DB tables and views are created via raw SQL and should not be affected
     1082by any ``manage.py`` actions.
     1083Note that if the initial SQL data is provided (see `Providing initial SQL
     1084data`_ below), it still will be present in the output of
     1085``sqlall``/``sqlcustom`` commands.
     1086
    10651087``db_tablespace``
    10661088-----------------
    10671089
Back to Top