Ticket #30511: schema.py

File schema.py, 12.6 KB (added by Michael Kany, 4 years ago)
Line 
1import psycopg2
2
3from django.db.backends.base.schema import BaseDatabaseSchemaEditor
4from django.conf import settings
5
6class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
7
8    sql_alter_column_type = "ALTER COLUMN %(column)s TYPE %(type)s USING %(column)s::%(type)s"
9
10    sql_create_sequence = "CREATE SEQUENCE %(sequence)s"
11    sql_delete_sequence = "DROP SEQUENCE IF EXISTS %(sequence)s CASCADE"
12    sql_set_sequence_max = "SELECT setval('%(sequence)s', MAX(%(column)s)) FROM %(table)s"
13
14    sql_create_index = "CREATE INDEX %(name)s ON %(table)s%(using)s (%(columns)s)%(extra)s"
15    sql_create_varchar_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s varchar_pattern_ops)%(extra)s"
16    sql_create_text_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s text_pattern_ops)%(extra)s"
17    sql_delete_index = "DROP INDEX IF EXISTS %(name)s"
18
19    # Setting the constraint to IMMEDIATE runs any deferred checks to allow
20    # dropping it in the same transaction.
21    sql_delete_fk = "SET CONSTRAINTS %(name)s IMMEDIATE; ALTER TABLE %(table)s DROP CONSTRAINT %(name)s"
22
23    sql_delete_procedure = 'DROP FUNCTION %(procedure)s(%(param_types)s)'
24
25    def quote_value(self, value):
26        return psycopg2.extensions.adapt(value)
27
28    def _field_indexes_sql(self, model, field):
29        output = super()._field_indexes_sql(model, field)
30        like_index_statement = self._create_like_index_sql(model, field)
31        if like_index_statement is not None:
32            output.append(like_index_statement)
33        return output
34
35    def _create_like_index_sql(self, model, field):
36        """
37        Return the statement to create an index with varchar operator pattern
38        when the column type is 'varchar' or 'text', otherwise return None.
39        """
40        db_type = field.db_type(connection=self.connection)
41        if db_type is not None and (field.db_index or field.unique):
42            # Fields with database column types of `varchar` and `text` need
43            # a second index that specifies their operator class, which is
44            # needed when performing correct LIKE queries outside the
45            # C locale. See #12234.
46            #
47            # The same doesn't apply to array fields such as varchar[size]
48            # and text[size], so skip them.
49            if '[' in db_type:
50                return None
51            if db_type.startswith('varchar'):
52                return self._create_index_sql(model, [field], suffix='_like', sql=self.sql_create_varchar_index)
53            elif db_type.startswith('text'):
54                return self._create_index_sql(model, [field], suffix='_like', sql=self.sql_create_text_index)
55        return None
56
57    def _alter_column_type_sql(self, model, old_field, new_field, new_type):
58        """Make ALTER TYPE with SERIAL make sense."""
59        table = model._meta.db_table
60        if new_type.lower() in ("serial", "bigserial"):
61            column = new_field.column
62            sequence_name = "%s_%s_seq" % (table, column)
63            col_type = "integer" if new_type.lower() == "serial" else "bigint"
64            return (
65                (
66                    self.sql_alter_column_type % {
67                        "column": self.quote_name(column),
68                        "type": col_type,
69                    },
70                    [],
71                ),
72                [
73                    (
74                        self.sql_delete_sequence % {
75                            "sequence": self.quote_name(sequence_name),
76                        },
77                        [],
78                    ),
79                    (
80                        self.sql_create_sequence % {
81                            "sequence": self.quote_name(sequence_name),
82                        },
83                        [],
84                    ),
85                    (
86                        # GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 5000 CACHE 1 ),
87                        # DEFAULT nextval('catalog_data_federalstate_id_seq'::regclass) ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
88
89                        self.sql_alter_column % {
90                            "table": self.quote_name(table),
91                            "changes": self.sql_alter_column_default % {
92                                "column": self.quote_name(column),
93                                "default": "nextval('%s')" % self.quote_name(sequence_name),
94                            }
95                        },
96                        [],
97                    ),
98                    (
99                        self.sql_set_sequence_max % {
100                            "table": self.quote_name(table),
101                            "column": self.quote_name(column),
102                            "sequence": self.quote_name(sequence_name),
103                        },
104                        [],
105                    ),
106                ],
107            )
108        else:
109            return super()._alter_column_type_sql(model, old_field, new_field, new_type)
110
111    def _alter_field(self, model, old_field, new_field, old_type, new_type,
112                     old_db_params, new_db_params, strict=False):
113        # Drop indexes on varchar/text/citext columns that are changing to a
114        # different type.
115        if (old_field.db_index or old_field.unique) and (
116            (old_type.startswith('varchar') and not new_type.startswith('varchar')) or
117            (old_type.startswith('text') and not new_type.startswith('text')) or
118            (old_type.startswith('citext') and not new_type.startswith('citext'))
119        ):
120            index_name = self._create_index_name(model._meta.db_table, [old_field.column], suffix='_like')
121            self.execute(self._delete_constraint_sql(self.sql_delete_index, model, index_name))
122
123        super()._alter_field(
124            model, old_field, new_field, old_type, new_type, old_db_params,
125            new_db_params, strict,
126        )
127        # Added an index? Create any PostgreSQL-specific indexes.
128        if ((not (old_field.db_index or old_field.unique) and new_field.db_index) or
129                (not old_field.unique and new_field.unique)):
130            like_index_statement = self._create_like_index_sql(model, new_field)
131            if like_index_statement is not None:
132                self.execute(like_index_statement)
133
134        # Removed an index? Drop any PostgreSQL-specific indexes.
135        if old_field.unique and not (new_field.db_index or new_field.unique):
136            index_to_remove = self._create_index_name(model._meta.db_table, [old_field.column], suffix='_like')
137            self.execute(self._delete_constraint_sql(self.sql_delete_index, model, index_to_remove))
138
139    def column_sql(self, model, field, include_default=False):
140        """
141        Take a field and return its column definition.
142        The field must already have had set_attributes_from_name() called.
143        """
144        # Get the column's type and use that as the basis of the SQL
145        db_params = field.db_parameters(connection=self.connection)
146        # Autoincrement SQL (for backends with post table definition variant)
147        if settings.DEBUG: print("=> " + field.get_internal_type(), end='')
148        sql = db_params['type']
149        if settings.DEBUG: print(" type: " + sql)
150        params = []
151        # Check for fields that aren't actually columns (e.g. M2M)
152        if sql is None:
153            return None, None
154        # Work out nullability
155        null = field.null
156        # If we were told to include a default value, do so
157        include_default = include_default and not self.skip_default(field)
158        if include_default:
159            default_value = self.effective_default(field)
160            if default_value is not None:
161                if self.connection.features.requires_literal_defaults:
162                    # Some databases can't take defaults as a parameter (oracle)
163                    # If this is the case, the individual schema backend should
164                    # implement prepare_default
165                    sql += " DEFAULT %s" % self.prepare_default(default_value)
166                else:
167                    sql += " DEFAULT %s"
168                    params += [default_value]
169        # Oracle treats the empty string ('') as null, so coerce the null
170        # option whenever '' is a possible value.
171        if (field.empty_strings_allowed and not field.primary_key and
172                self.connection.features.interprets_empty_strings_as_nulls):
173            null = True
174        if null and not self.connection.features.implied_column_null:
175            sql += " NULL"
176        elif not null:
177            sql += " NOT NULL"
178        # Primary key/unique outputs
179        if field.primary_key:
180            sql += " PRIMARY KEY"
181            if field.get_internal_type() in ("IdentityAutoField", "IdentityBigAutoField"):
182                sql += " GENERATED ALWAYS AS IDENTITY"
183        elif field.unique:
184            sql += " UNIQUE"
185        # Optionally add the tablespace if it's an implicitly indexed column
186        tablespace = field.db_tablespace or model._meta.db_tablespace
187        if tablespace and self.connection.features.supports_tablespaces and field.unique:
188            sql += " %s" % self.connection.ops.tablespace_sql(tablespace, inline=True)
189        # Return the sql
190        return sql, params
191
192    def create_model(self, model):
193        """
194        Create a table and any accompanying indexes or unique constraints for
195        the given `model`.
196        """
197        # Create column SQL, add FK deferreds if needed
198        column_sqls = []
199        params = []
200        if settings.DEBUG: print('')
201        for field in model._meta.local_fields:
202            # SQL
203            definition, extra_params = self.column_sql(model, field)
204            if definition is None:
205                continue
206            # Check constraints can go on the column SQL here
207            db_params = field.db_parameters(connection=self.connection)
208            if db_params['check']:
209                definition += " CHECK (%s)" % db_params['check']
210            # Autoincrement SQL (for backends with inline variant)
211            col_type_suffix = field.db_type_suffix(connection=self.connection)
212            if col_type_suffix:
213                definition += " %s" % col_type_suffix
214            params.extend(extra_params)
215            # FK
216            if field.remote_field and field.db_constraint:
217                to_table = field.remote_field.model._meta.db_table
218                to_column = field.remote_field.model._meta.get_field(field.remote_field.field_name).column
219                if self.sql_create_inline_fk:
220                    definition += " " + self.sql_create_inline_fk % {
221                        "to_table": self.quote_name(to_table),
222                        "to_column": self.quote_name(to_column),
223                    }
224                elif self.connection.features.supports_foreign_keys:
225                    self.deferred_sql.append(self._create_fk_sql(model, field, "_fk_%(to_table)s_%(to_column)s"))
226            # Add the SQL to our big list
227            column_sqls.append("%s %s" % (
228                self.quote_name(field.column),
229                definition,
230            ))
231            # Autoincrement SQL (for backends with post table definition variant)
232            if field.get_internal_type() in ("AutoField", "BigAutoField"):
233                autoinc_sql = self.connection.ops.autoinc_sql(model._meta.db_table, field.column)
234                if autoinc_sql:
235                    self.deferred_sql.extend(autoinc_sql)
236
237        # Add any unique_togethers (always deferred, as some fields might be
238        # created afterwards, like geometry fields with some backends)
239        for fields in model._meta.unique_together:
240            columns = [model._meta.get_field(field).column for field in fields]
241            self.deferred_sql.append(self._create_unique_sql(model, columns))
242        # Make the table
243        sql = self.sql_create_table % {
244            "table": self.quote_name(model._meta.db_table),
245            "definition": ", ".join(column_sqls)
246        }
247        if model._meta.db_tablespace:
248            tablespace_sql = self.connection.ops.tablespace_sql(model._meta.db_tablespace)
249            if tablespace_sql:
250                sql += ' ' + tablespace_sql
251        # Prevent using [] as params, in the case a literal '%' is used in the definition
252        self.execute(sql, params or None)
253        if settings.DEBUG: print("=> "+ str(sql))
254
255        # Add any field index and index_together's (deferred as SQLite3 _remake_table needs it)
256        self.deferred_sql.extend(self._model_indexes_sql(model))
257
258        # Make M2M tables
259        for field in model._meta.local_many_to_many:
260            if field.remote_field.through._meta.auto_created:
261                self.create_model(field.remote_field.through)
Back to Top