Code

Ticket #3163: create_db_schema-trunk7020-withtests.diff

File create_db_schema-trunk7020-withtests.diff, 20.2 KB (added by honeyman, 6 years ago)

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

Line 
1Index: django/db/models/options.py
2===================================================================
3--- django/db/models/options.py (revision 7020)
4+++ django/db/models/options.py (working copy)
5@@ -15,7 +15,7 @@
6 
7 DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
8                  'unique_together', 'permissions', 'get_latest_by',
9-                 'order_with_respect_to', 'app_label', 'db_tablespace')
10+                 'order_with_respect_to', 'app_label', 'db_tablespace', 'create_db_schema')
11 
12 class Options(object):
13     def __init__(self, meta):
14@@ -30,6 +30,7 @@
15         self.get_latest_by = None
16         self.order_with_respect_to = None
17         self.db_tablespace = settings.DEFAULT_TABLESPACE
18+        self.create_db_schema = True
19         self.admin = None
20         self.meta = meta
21         self.pk = None
22Index: django/core/management/sql.py
23===================================================================
24--- django/core/management/sql.py       (revision 7020)
25+++ django/core/management/sql.py       (working copy)
26@@ -13,20 +13,24 @@
27     cursor = connection.cursor()
28     return get_introspection_module().get_table_list(cursor)
29 
30-def django_table_list(only_existing=False):
31+def django_table_list(only_existing=False, filter_not_generated_tables=False):
32     """
33     Returns a list of all table names that have associated Django models and
34     are in INSTALLED_APPS.
35 
36     If only_existing is True, the resulting list will only include the tables
37     that actually exist in the database.
38+
39+    If filter_not_generated_tables is True, then all tables with associated Django models
40+    which have Meta option create_db_schema=False will not be added to the list.
41     """
42     from django.db import models
43     tables = []
44     for app in models.get_apps():
45         for model in models.get_models(app):
46-            tables.append(model._meta.db_table)
47-            tables.extend([f.m2m_db_table() for f in model._meta.many_to_many])
48+            if (not filter_not_generated_tables) or (model._meta.create_db_schema):
49+                tables.append(model._meta.db_table)
50+                tables.extend([f.m2m_db_table() for f in model._meta.many_to_many])
51     if only_existing:
52         existing = table_list()
53         tables = [t for t in tables if t in existing]
54@@ -45,8 +49,13 @@
55         converter = lambda x: x
56     return set([m for m in all_models if converter(m._meta.db_table) in map(converter, table_list)])
57 
58-def sequence_list():
59-    "Returns a list of information about all DB sequences for all models in all apps."
60+def sequence_list(filter_not_generated_tables=False):
61+    """
62+    Returns a list of information about all DB sequences for all models in all apps.
63+
64+    If filter_not_generated_tables is True, then only the sequences for the Django models
65+    which have Meta option create_db_schema=False will be added to the list.
66+    """
67     from django.db import models
68 
69     apps = models.get_apps()
70@@ -54,10 +63,11 @@
71 
72     for app in apps:
73         for model in models.get_models(app):
74-            for f in model._meta.fields:
75-                if isinstance(f, models.AutoField):
76-                    sequence_list.append({'table': model._meta.db_table, 'column': f.column})
77-                    break # Only one AutoField is allowed per model, so don't bother continuing.
78+            if (not filter_not_generated_tables) or (model._meta.create_db_schema):
79+                for f in model._meta.fields:
80+                    if isinstance(f, models.AutoField):
81+                        sequence_list.append({'table': model._meta.db_table, 'column': f.column})
82+                        break # Only one AutoField is allowed per model, so don't bother continuing.
83 
84             for f in model._meta.many_to_many:
85                 sequence_list.append({'table': f.m2m_db_table(), 'column': None})
86@@ -156,8 +166,9 @@
87     for model in app_models:
88         if cursor and table_name_converter(model._meta.db_table) in table_names:
89             # Drop the table now
90-            output.append('%s %s;' % (style.SQL_KEYWORD('DROP TABLE'),
91-                style.SQL_TABLE(qn(model._meta.db_table))))
92+            if model._meta.create_db_schema:
93+                output.append('%s %s;' % (style.SQL_KEYWORD('DROP TABLE'),
94+                    style.SQL_TABLE(qn(model._meta.db_table))))
95             if connection.features.supports_constraints and model in references_to_delete:
96                 for rel_class, f in references_to_delete[model]:
97                     table = rel_class._meta.db_table
98@@ -165,11 +176,12 @@
99                     r_table = model._meta.db_table
100                     r_col = model._meta.get_field(f.rel.field_name).column
101                     r_name = '%s_refs_%s_%x' % (col, r_col, abs(hash((table, r_table))))
102-                    output.append('%s %s %s %s;' % \
103-                        (style.SQL_KEYWORD('ALTER TABLE'),
104-                        style.SQL_TABLE(qn(table)),
105-                        style.SQL_KEYWORD(connection.ops.drop_foreignkey_sql()),
106-                        style.SQL_FIELD(truncate_name(r_name, connection.ops.max_name_length()))))
107+                    if rel_class._meta.create_db_schema:
108+                        output.append('%s %s %s %s;' % \
109+                            (style.SQL_KEYWORD('ALTER TABLE'),
110+                            style.SQL_TABLE(qn(table)),
111+                            style.SQL_KEYWORD(connection.ops.drop_foreignkey_sql()),
112+                            style.SQL_FIELD(truncate_name(r_name, connection.ops.max_name_length()))))
113                 del references_to_delete[model]
114             if model._meta.has_auto_field:
115                 ds = connection.ops.drop_sequence_sql(model._meta.db_table)
116@@ -206,16 +218,16 @@
117 def sql_flush(style, only_django=False):
118     """
119     Returns a list of the SQL statements used to flush the database.
120-   
121+
122     If only_django is True, then only table names that have associated Django
123     models and are in INSTALLED_APPS will be included.
124     """
125     from django.db import connection
126     if only_django:
127-        tables = django_table_list()
128+        tables = django_table_list(filter_not_generated_tables=True)
129     else:
130         tables = table_list()
131-    statements = connection.ops.sql_flush(style, tables, sequence_list())
132+    statements = connection.ops.sql_flush(style, tables, sequence_list(filter_not_generated_tables=True))
133     return statements
134 
135 def sql_custom(app):
136@@ -295,22 +307,24 @@
137         table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \
138             ", ".join([style.SQL_FIELD(qn(opts.get_field(f).column)) for f in field_constraints]))
139 
140-    full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' (']
141-    for i, line in enumerate(table_output): # Combine and add commas.
142-        full_statement.append('    %s%s' % (line, i < len(table_output)-1 and ',' or ''))
143-    full_statement.append(')')
144-    if opts.db_tablespace and connection.features.supports_tablespaces:
145-        full_statement.append(connection.ops.tablespace_sql(opts.db_tablespace))
146-    full_statement.append(';')
147-    final_output.append('\n'.join(full_statement))
148+    # Now build up the CREATE TABLE section but only if the model requires it
149+    if opts.create_db_schema:
150+        full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' (']
151+        for i, line in enumerate(table_output): # Combine and add commas.
152+            full_statement.append('    %s%s' % (line, i < len(table_output)-1 and ',' or ''))
153+        full_statement.append(')')
154+        if opts.db_tablespace and connection.features.supports_tablespaces:
155+            full_statement.append(connection.ops.tablespace_sql(opts.db_tablespace))
156+        full_statement.append(';')
157+        final_output.append('\n'.join(full_statement))
158 
159-    if opts.has_auto_field:
160-        # Add any extra SQL needed to support auto-incrementing primary keys.
161-        auto_column = opts.auto_field.db_column or opts.auto_field.name
162-        autoinc_sql = connection.ops.autoinc_sql(opts.db_table, auto_column)
163-        if autoinc_sql:
164-            for stmt in autoinc_sql:
165-                final_output.append(stmt)
166+        if opts.has_auto_field:
167+            # Add any extra SQL needed to support auto-incrementing primary keys.
168+            auto_column = opts.auto_field.db_column or opts.auto_field.name
169+            autoinc_sql = connection.ops.autoinc_sql(opts.db_table, auto_column)
170+            if autoinc_sql:
171+                for stmt in autoinc_sql:
172+                    final_output.append(stmt)
173 
174     return final_output, pending_references
175 
176@@ -335,10 +349,11 @@
177                 # For MySQL, r_name must be unique in the first 64 characters.
178                 # So we are careful with character usage here.
179                 r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table))))
180-                final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
181-                    (qn(r_table), truncate_name(r_name, connection.ops.max_name_length()),
182-                    qn(r_col), qn(table), qn(col),
183-                    connection.ops.deferrable_sql()))
184+                if rel_opts.create_db_schema:
185+                    final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
186+                        (qn(r_table), truncate_name(r_name, connection.ops.max_name_length()),
187+                        qn(r_col), qn(table), qn(col),
188+                        connection.ops.deferrable_sql()))
189             del pending_references[model]
190     return final_output
191 
192@@ -411,7 +426,7 @@
193             for r_table, r_col, table, col in deferred:
194                 r_name = '%s_refs_%s_%x' % (r_col, col,
195                         abs(hash((r_table, table))))
196-                final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' %
197+                final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' %
198                 (qn(r_table),
199                 truncate_name(r_name, connection.ops.max_name_length()),
200                 qn(r_col), qn(table), qn(col),
201@@ -458,22 +473,23 @@
202     output = []
203 
204     qn = connection.ops.quote_name
205-    for f in model._meta.fields:
206-        if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys):
207-            unique = f.unique and 'UNIQUE ' or ''
208-            tablespace = f.db_tablespace or model._meta.db_tablespace
209-            if tablespace and connection.features.supports_tablespaces:
210-                tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace)
211-            else:
212-                tablespace_sql = ''
213-            output.append(
214-                style.SQL_KEYWORD('CREATE %sINDEX' % unique) + ' ' + \
215-                style.SQL_TABLE(qn('%s_%s' % (model._meta.db_table, f.column))) + ' ' + \
216-                style.SQL_KEYWORD('ON') + ' ' + \
217-                style.SQL_TABLE(qn(model._meta.db_table)) + ' ' + \
218-                "(%s)" % style.SQL_FIELD(qn(f.column)) + \
219-                "%s;" % tablespace_sql
220-            )
221+    if model._meta.create_db_schema:
222+        for f in model._meta.fields:
223+            if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys):
224+                unique = f.unique and 'UNIQUE ' or ''
225+                tablespace = f.db_tablespace or model._meta.db_tablespace
226+                if tablespace and connection.features.supports_tablespaces:
227+                    tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace)
228+                else:
229+                    tablespace_sql = ''
230+                output.append(
231+                    style.SQL_KEYWORD('CREATE %sINDEX' % unique) + ' ' + \
232+                    style.SQL_TABLE(qn('%s_%s' % (model._meta.db_table, f.column))) + ' ' + \
233+                    style.SQL_KEYWORD('ON') + ' ' + \
234+                    style.SQL_TABLE(qn(model._meta.db_table)) + ' ' + \
235+                    "(%s)" % style.SQL_FIELD(qn(f.column)) + \
236+                    "%s;" % tablespace_sql
237+                )
238     return output
239 
240 def emit_post_sync_signal(created_models, verbosity, interactive):
241Index: tests/modeltests/create_db_schema/__init__.py
242===================================================================
243Index: tests/modeltests/create_db_schema/models.py
244===================================================================
245--- tests/modeltests/create_db_schema/models.py (revision 0)
246+++ tests/modeltests/create_db_schema/models.py (revision 0)
247@@ -0,0 +1,201 @@
248+"""
249+xx. create_db_schema
250+
251+Models can have a ``create_db_schema`` attribute, which specifies
252+whether the SQL code is generated for the table on various manage.py operations
253+or not.
254+"""
255+
256+from django.db import models
257+
258+"""
259+General test strategy:
260+* All tests are numbered (01, 02, 03... etc).
261+* Each test contains three models (A, B, C, followed with the number of test),
262+  containing both indexed and non-indexed fields (to verify sql_index),
263+  usual fields (model A), foreign keys (model B) and many-to-many fields (model C).
264+  D table is generated automatically as intermediate M2M one.
265+* The normal (default; create_db_schema = True) behaviour during the manage.py
266+  operations is not thoroughly checked; it is the duty of the appropriate tests
267+  for the primary functionality of these operations.
268+  The most attention is paid to whether the create_db_schema = False
269+  disables the SQL generation properly.
270+* The intermediate table for M2M relations is not ever verified explicitly,
271+  because it is not ever marked with create_db_schema explicitly.
272+"""
273+
274+# This dictionary maps the name of the model/SQL table (like 'A01')
275+# to the boolean specifying whether this name should appear in the final SQL
276+checks = {}
277+
278+
279+"""
280+01: create_db_schema is not set.
281+    In such case, it should be equal (by default) to True,
282+    and SQL is generated for all three models.
283+"""
284+checks['A01'] = True
285+checks['B01'] = True
286+checks['C01'] = True
287+
288+class A01(models.Model):
289+    class Meta: db_table = 'A01'
290+
291+    f_a = models.TextField(db_index = True)
292+    f_b = models.IntegerField()
293+
294+class B01(models.Model):
295+    class Meta: db_table = 'B01'
296+
297+    fk_a = models.ForeignKey(A01)
298+    f_a = models.TextField(db_index = True)
299+    f_b = models.IntegerField()
300+
301+class C01(models.Model):
302+    class Meta: db_table = 'C01'
303+
304+    mm_a = models.ManyToManyField(A01, db_table = 'D01')
305+    f_a = models.TextField(db_index = True)
306+    f_b = models.IntegerField()
307+
308+
309+"""
310+02: create_db_schema is set to True.
311+    SQL is generated for all three models.
312+"""
313+checks['A02'] = True
314+checks['B02'] = True
315+checks['C02'] = True
316+
317+class A02(models.Model):
318+    class Meta:
319+        db_table = 'A02'
320+        create_db_schema = True
321+
322+    f_a = models.TextField(db_index = True)
323+    f_b = models.IntegerField()
324+
325+class B02(models.Model):
326+    class Meta:
327+        db_table = 'B02'
328+        create_db_schema = True
329+
330+    fk_a = models.ForeignKey(A02)
331+    f_a = models.TextField(db_index = True)
332+    f_b = models.IntegerField()
333+
334+class C02(models.Model):
335+    class Meta:
336+        db_table = 'C02'
337+        create_db_schema = True
338+
339+    mm_a = models.ManyToManyField(A02, db_table = 'D02')
340+    f_a = models.TextField(db_index = True)
341+    f_b = models.IntegerField()
342+
343+
344+"""
345+03: create_db_schema is set to False.
346+    SQL is NOT generated for any of the three models.
347+"""
348+checks['A03'] = False
349+checks['B03'] = False
350+checks['C03'] = False
351+
352+class A03(models.Model):
353+    class Meta:
354+        db_table = 'A03'
355+        create_db_schema = False
356+
357+    f_a = models.TextField(db_index = True)
358+    f_b = models.IntegerField()
359+
360+class B03(models.Model):
361+    class Meta:
362+        db_table = 'B03'
363+        create_db_schema = False
364+
365+    fk_a = models.ForeignKey(A03)
366+    f_a = models.TextField(db_index = True)
367+    f_b = models.IntegerField()
368+
369+class C03(models.Model):
370+    class Meta:
371+        db_table = 'C03'
372+        create_db_schema = False
373+
374+    mm_a = models.ManyToManyField(A03, db_table = 'D03')
375+    f_a = models.TextField(db_index = True)
376+    f_b = models.IntegerField()
377+
378+
379+# We will use short names for these templates
380+sql_templates = {
381+       'create table': 'CREATE TABLE "%s"',
382+       'create index': 'CREATE INDEX "%s_f_a"',
383+       'drop table': 'DROP TABLE "%s"',
384+       'delete from': 'DELETE FROM "%s"'
385+}
386+
387+def get_failed_models(arr_sql, sql_template_names):
388+    """
389+    Find the models which should not be in the SQL but they are present,
390+    or they should be in the SQL but they are missing.
391+    """
392+    txt_sql = ' '.join(arr_sql)
393+    for (model, should_be_present) in checks.iteritems():
394+        # Do we expect to see the model name in the SQL text?
395+        for sql_template_name in sql_template_names:
396+            # We are interested not in just the model name like "A01",
397+            # but in the whole string like 'CREATE TABLE "A01"'
398+            # so we apply the model name to the template
399+            # to find out the expected string
400+            expected = (sql_templates[sql_template_name])%model
401+            if ((expected in txt_sql) != should_be_present):
402+                # Our expectations failed!
403+                yield 'The string %s %s present in SQL but it %s.'%(
404+                    expected,
405+                    {False: 'is not', True: 'is'}[expected in txt_sql],
406+                    {False: 'should not be', True: 'should be'}[should_be_present]
407+                    )
408+
409+
410+__test__ = {'API_TESTS':"""
411+>>> from django.db.models import get_app
412+>>> from django.core.management.sql import *
413+>>> from django.core.management.color import no_style
414+>>> import sys
415+
416+>>> myapp = get_app('create_db_schema')
417+>>> mystyle = no_style()
418+
419+# a. Verify sql_create
420+>>> list(get_failed_models( sql_create(myapp, mystyle), ['create table'] ))
421+[]
422+
423+# b. Verify sql_delete
424+>>> list(get_failed_models( sql_delete(myapp, mystyle), ['drop table'] ))
425+[]
426+
427+# c. Verify sql_reset
428+>>> list(get_failed_models( sql_reset(myapp, mystyle), ['drop table', 'create table', 'create index'] ))
429+[]
430+
431+# d. Verify sql_flush
432+>>> # sql_flush(mystyle)
433+>>> list(get_failed_models( sql_flush(mystyle), ['delete from'] ))
434+[]
435+
436+# e. Verify sql_custom
437+# No custom data provided, should not be no output.
438+>>> sql_custom(myapp)
439+[]
440+
441+# f. Verify sql_indexes
442+>>> list(get_failed_models( sql_indexes(myapp, mystyle), ['create index'] ))
443+[]
444+
445+# g. Verify sql_all
446+>>> list(get_failed_models( sql_all(myapp, mystyle), ['create table', 'create index'] ))
447+[]
448+"""}
449Index: AUTHORS
450===================================================================
451--- AUTHORS     (revision 7020)
452+++ AUTHORS     (working copy)
453@@ -353,6 +353,7 @@
454     ymasuda@ethercube.com
455     Jarek Zgoda <jarek.zgoda@gmail.com>
456     Cheng Zhang
457+    Alexander Myodov <amyodov@gmail.com>
458 
459 A big THANK YOU goes to:
460 
461Index: docs/model-api.txt
462===================================================================
463--- docs/model-api.txt  (revision 7020)
464+++ docs/model-api.txt  (working copy)
465@@ -1051,6 +1051,28 @@
466 that aren't allowed in Python variable names -- notably, the hyphen --
467 that's OK. Django quotes column and table names behind the scenes.
468 
469+``create_db_schema``
470+--------------------
471+
472+**New in Django development version**
473+
474+Marks this model as requiring SQL operations when calling ``manage.py``::
475+
476+    create_db_schema = False
477+
478+If this isn't given, Django will use ``create_db_schema = True``
479+what means that the operations like ``manage.py sqlreset``, ``manage.py syncdb``
480+and others will regenerate the appropriate table when needed.
481+If the option is set to False, the appropriate table will not be affected with
482+any SQL operations.
483+
484+This is useful for databases where some DB tables are controlled by Django models,
485+but some other DB tables and views are created via raw SQL and should not be affected
486+by any ``manage.py`` actions.
487+Note that if the initial SQL data is provided (see `Providing initial SQL
488+data`_ below), it still will be present in the output of
489+``sqlall``/``sqlcustom`` commands.
490+
491 ``db_tablespace``
492 -----------------
493