Fixed #30511 -- Used identity columns instead of serials on PostgreSQL.
This commit is contained in:
parent
62ffc9883a
commit
2eea361eff
|
@ -904,6 +904,9 @@ class BaseDatabaseSchemaEditor:
|
||||||
actions = []
|
actions = []
|
||||||
null_actions = []
|
null_actions = []
|
||||||
post_actions = []
|
post_actions = []
|
||||||
|
# Type suffix change? (e.g. auto increment).
|
||||||
|
old_type_suffix = old_field.db_type_suffix(connection=self.connection)
|
||||||
|
new_type_suffix = new_field.db_type_suffix(connection=self.connection)
|
||||||
# Collation change?
|
# Collation change?
|
||||||
old_collation = getattr(old_field, "db_collation", None)
|
old_collation = getattr(old_field, "db_collation", None)
|
||||||
new_collation = getattr(new_field, "db_collation", None)
|
new_collation = getattr(new_field, "db_collation", None)
|
||||||
|
@ -914,7 +917,7 @@ class BaseDatabaseSchemaEditor:
|
||||||
)
|
)
|
||||||
actions.append(fragment)
|
actions.append(fragment)
|
||||||
# Type change?
|
# Type change?
|
||||||
elif old_type != new_type:
|
elif (old_type, old_type_suffix) != (new_type, new_type_suffix):
|
||||||
fragment, other_actions = self._alter_column_type_sql(
|
fragment, other_actions = self._alter_column_type_sql(
|
||||||
model, old_field, new_field, new_type
|
model, old_field, new_field, new_type
|
||||||
)
|
)
|
||||||
|
|
|
@ -72,8 +72,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
# be interpolated against the values of Field.__dict__ before being output.
|
# be interpolated against the values of Field.__dict__ before being output.
|
||||||
# If a column type is set to None, it won't be included in the output.
|
# If a column type is set to None, it won't be included in the output.
|
||||||
data_types = {
|
data_types = {
|
||||||
"AutoField": "serial",
|
"AutoField": "integer",
|
||||||
"BigAutoField": "bigserial",
|
"BigAutoField": "bigint",
|
||||||
"BinaryField": "bytea",
|
"BinaryField": "bytea",
|
||||||
"BooleanField": "boolean",
|
"BooleanField": "boolean",
|
||||||
"CharField": "varchar(%(max_length)s)",
|
"CharField": "varchar(%(max_length)s)",
|
||||||
|
@ -94,7 +94,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
"PositiveIntegerField": "integer",
|
"PositiveIntegerField": "integer",
|
||||||
"PositiveSmallIntegerField": "smallint",
|
"PositiveSmallIntegerField": "smallint",
|
||||||
"SlugField": "varchar(%(max_length)s)",
|
"SlugField": "varchar(%(max_length)s)",
|
||||||
"SmallAutoField": "smallserial",
|
"SmallAutoField": "smallint",
|
||||||
"SmallIntegerField": "smallint",
|
"SmallIntegerField": "smallint",
|
||||||
"TextField": "text",
|
"TextField": "text",
|
||||||
"TimeField": "time",
|
"TimeField": "time",
|
||||||
|
@ -105,6 +105,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
"PositiveIntegerField": '"%(column)s" >= 0',
|
"PositiveIntegerField": '"%(column)s" >= 0',
|
||||||
"PositiveSmallIntegerField": '"%(column)s" >= 0',
|
"PositiveSmallIntegerField": '"%(column)s" >= 0',
|
||||||
}
|
}
|
||||||
|
data_types_suffix = {
|
||||||
|
"AutoField": "GENERATED BY DEFAULT AS IDENTITY",
|
||||||
|
"BigAutoField": "GENERATED BY DEFAULT AS IDENTITY",
|
||||||
|
"SmallAutoField": "GENERATED BY DEFAULT AS IDENTITY",
|
||||||
|
}
|
||||||
operators = {
|
operators = {
|
||||||
"exact": "= %s",
|
"exact": "= %s",
|
||||||
"iexact": "= UPPER(%s)",
|
"iexact": "= UPPER(%s)",
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
from django.db.backends.base.introspection import (
|
from collections import namedtuple
|
||||||
BaseDatabaseIntrospection,
|
|
||||||
FieldInfo,
|
from django.db.backends.base.introspection import BaseDatabaseIntrospection
|
||||||
TableInfo,
|
from django.db.backends.base.introspection import FieldInfo as BaseFieldInfo
|
||||||
)
|
from django.db.backends.base.introspection import TableInfo
|
||||||
from django.db.models import Index
|
from django.db.models import Index
|
||||||
|
|
||||||
|
FieldInfo = namedtuple("FieldInfo", BaseFieldInfo._fields + ("is_autofield",))
|
||||||
|
|
||||||
|
|
||||||
class DatabaseIntrospection(BaseDatabaseIntrospection):
|
class DatabaseIntrospection(BaseDatabaseIntrospection):
|
||||||
# Maps type codes to Django Field types.
|
# Maps type codes to Django Field types.
|
||||||
|
@ -37,7 +39,11 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
||||||
|
|
||||||
def get_field_type(self, data_type, description):
|
def get_field_type(self, data_type, description):
|
||||||
field_type = super().get_field_type(data_type, description)
|
field_type = super().get_field_type(data_type, description)
|
||||||
if description.default and "nextval" in description.default:
|
if description.is_autofield or (
|
||||||
|
# Required for pre-Django 4.1 serial columns.
|
||||||
|
description.default
|
||||||
|
and "nextval" in description.default
|
||||||
|
):
|
||||||
if field_type == "IntegerField":
|
if field_type == "IntegerField":
|
||||||
return "AutoField"
|
return "AutoField"
|
||||||
elif field_type == "BigIntegerField":
|
elif field_type == "BigIntegerField":
|
||||||
|
@ -84,7 +90,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
||||||
a.attname AS column_name,
|
a.attname AS column_name,
|
||||||
NOT (a.attnotnull OR (t.typtype = 'd' AND t.typnotnull)) AS is_nullable,
|
NOT (a.attnotnull OR (t.typtype = 'd' AND t.typnotnull)) AS is_nullable,
|
||||||
pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
|
pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
|
||||||
CASE WHEN collname = 'default' THEN NULL ELSE collname END AS collation
|
CASE WHEN collname = 'default' THEN NULL ELSE collname END AS collation,
|
||||||
|
a.attidentity != '' AS is_autofield
|
||||||
FROM pg_attribute a
|
FROM pg_attribute a
|
||||||
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
|
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
|
||||||
LEFT JOIN pg_collation co ON a.attcollation = co.oid
|
LEFT JOIN pg_collation co ON a.attcollation = co.oid
|
||||||
|
@ -118,23 +125,21 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
||||||
def get_sequences(self, cursor, table_name, table_fields=()):
|
def get_sequences(self, cursor, table_name, table_fields=()):
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT s.relname as sequence_name, col.attname
|
SELECT
|
||||||
FROM pg_class s
|
s.relname AS sequence_name,
|
||||||
JOIN pg_namespace sn ON sn.oid = s.relnamespace
|
a.attname AS colname
|
||||||
JOIN
|
FROM
|
||||||
pg_depend d ON d.refobjid = s.oid
|
pg_class s
|
||||||
|
JOIN pg_depend d ON d.objid = s.oid
|
||||||
|
AND d.classid = 'pg_class'::regclass
|
||||||
AND d.refclassid = 'pg_class'::regclass
|
AND d.refclassid = 'pg_class'::regclass
|
||||||
JOIN
|
JOIN pg_attribute a ON d.refobjid = a.attrelid
|
||||||
pg_attrdef ad ON ad.oid = d.objid
|
AND d.refobjsubid = a.attnum
|
||||||
AND d.classid = 'pg_attrdef'::regclass
|
JOIN pg_class tbl ON tbl.oid = d.refobjid
|
||||||
JOIN
|
|
||||||
pg_attribute col ON col.attrelid = ad.adrelid
|
|
||||||
AND col.attnum = ad.adnum
|
|
||||||
JOIN pg_class tbl ON tbl.oid = ad.adrelid
|
|
||||||
WHERE s.relkind = 'S'
|
|
||||||
AND d.deptype in ('a', 'n')
|
|
||||||
AND pg_catalog.pg_table_is_visible(tbl.oid)
|
|
||||||
AND tbl.relname = %s
|
AND tbl.relname = %s
|
||||||
|
AND pg_catalog.pg_table_is_visible(tbl.oid)
|
||||||
|
WHERE
|
||||||
|
s.relkind = 'S';
|
||||||
""",
|
""",
|
||||||
[table_name],
|
[table_name],
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,12 +7,7 @@ from django.db.backends.utils import strip_quotes
|
||||||
|
|
||||||
class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
|
|
||||||
sql_create_sequence = "CREATE SEQUENCE %(sequence)s"
|
|
||||||
sql_delete_sequence = "DROP SEQUENCE IF EXISTS %(sequence)s CASCADE"
|
sql_delete_sequence = "DROP SEQUENCE IF EXISTS %(sequence)s CASCADE"
|
||||||
sql_set_sequence_max = (
|
|
||||||
"SELECT setval('%(sequence)s', MAX(%(column)s)) FROM %(table)s"
|
|
||||||
)
|
|
||||||
sql_set_sequence_owner = "ALTER SEQUENCE %(sequence)s OWNED BY %(table)s.%(column)s"
|
|
||||||
|
|
||||||
sql_create_index = (
|
sql_create_index = (
|
||||||
"CREATE INDEX %(name)s ON %(table)s%(using)s "
|
"CREATE INDEX %(name)s ON %(table)s%(using)s "
|
||||||
|
@ -39,6 +34,14 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
)
|
)
|
||||||
sql_delete_procedure = "DROP FUNCTION %(procedure)s(%(param_types)s)"
|
sql_delete_procedure = "DROP FUNCTION %(procedure)s(%(param_types)s)"
|
||||||
|
|
||||||
|
sql_add_identity = (
|
||||||
|
"ALTER TABLE %(table)s ALTER COLUMN %(column)s ADD "
|
||||||
|
"GENERATED BY DEFAULT AS IDENTITY"
|
||||||
|
)
|
||||||
|
sql_drop_indentity = (
|
||||||
|
"ALTER TABLE %(table)s ALTER COLUMN %(column)s DROP IDENTITY IF EXISTS"
|
||||||
|
)
|
||||||
|
|
||||||
def quote_value(self, value):
|
def quote_value(self, value):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = value.replace("%", "%%")
|
value = value.replace("%", "%%")
|
||||||
|
@ -116,78 +119,47 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
self.sql_alter_column_type += using_sql
|
self.sql_alter_column_type += using_sql
|
||||||
elif self._field_data_type(old_field) != self._field_data_type(new_field):
|
elif self._field_data_type(old_field) != self._field_data_type(new_field):
|
||||||
self.sql_alter_column_type += using_sql
|
self.sql_alter_column_type += using_sql
|
||||||
# Make ALTER TYPE with SERIAL make sense.
|
# Make ALTER TYPE with IDENTITY make sense.
|
||||||
table = strip_quotes(model._meta.db_table)
|
table = strip_quotes(model._meta.db_table)
|
||||||
serial_fields_map = {
|
auto_field_types = {
|
||||||
"bigserial": "bigint",
|
"AutoField",
|
||||||
"serial": "integer",
|
"BigAutoField",
|
||||||
"smallserial": "smallint",
|
"SmallAutoField",
|
||||||
}
|
}
|
||||||
if new_type.lower() in serial_fields_map:
|
old_is_auto = old_internal_type in auto_field_types
|
||||||
|
new_is_auto = new_internal_type in auto_field_types
|
||||||
|
if new_is_auto and not old_is_auto:
|
||||||
column = strip_quotes(new_field.column)
|
column = strip_quotes(new_field.column)
|
||||||
sequence_name = "%s_%s_seq" % (table, column)
|
|
||||||
return (
|
return (
|
||||||
(
|
(
|
||||||
self.sql_alter_column_type
|
self.sql_alter_column_type
|
||||||
% {
|
% {
|
||||||
"column": self.quote_name(column),
|
"column": self.quote_name(column),
|
||||||
"type": serial_fields_map[new_type.lower()],
|
"type": new_type,
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
self.sql_delete_sequence
|
self.sql_add_identity
|
||||||
% {
|
|
||||||
"sequence": self.quote_name(sequence_name),
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
self.sql_create_sequence
|
|
||||||
% {
|
|
||||||
"sequence": self.quote_name(sequence_name),
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
self.sql_alter_column
|
|
||||||
% {
|
|
||||||
"table": self.quote_name(table),
|
|
||||||
"changes": self.sql_alter_column_default
|
|
||||||
% {
|
|
||||||
"column": self.quote_name(column),
|
|
||||||
"default": "nextval('%s')"
|
|
||||||
% self.quote_name(sequence_name),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
self.sql_set_sequence_max
|
|
||||||
% {
|
% {
|
||||||
"table": self.quote_name(table),
|
"table": self.quote_name(table),
|
||||||
"column": self.quote_name(column),
|
"column": self.quote_name(column),
|
||||||
"sequence": self.quote_name(sequence_name),
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
self.sql_set_sequence_owner
|
|
||||||
% {
|
|
||||||
"table": self.quote_name(table),
|
|
||||||
"column": self.quote_name(column),
|
|
||||||
"sequence": self.quote_name(sequence_name),
|
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
elif (
|
elif old_is_auto and not new_is_auto:
|
||||||
old_field.db_parameters(connection=self.connection)["type"]
|
# Drop IDENTITY if exists (pre-Django 4.1 serial columns don't have
|
||||||
in serial_fields_map
|
# it).
|
||||||
):
|
self.execute(
|
||||||
# Drop the sequence if migrating away from AutoField.
|
self.sql_drop_indentity
|
||||||
|
% {
|
||||||
|
"table": self.quote_name(table),
|
||||||
|
"column": self.quote_name(strip_quotes(old_field.column)),
|
||||||
|
}
|
||||||
|
)
|
||||||
column = strip_quotes(new_field.column)
|
column = strip_quotes(new_field.column)
|
||||||
sequence_name = "%s_%s_seq" % (table, column)
|
sequence_name = "%s_%s_seq" % (table, column)
|
||||||
fragment, _ = super()._alter_column_type_sql(
|
fragment, _ = super()._alter_column_type_sql(
|
||||||
|
@ -195,6 +167,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
)
|
)
|
||||||
return fragment, [
|
return fragment, [
|
||||||
(
|
(
|
||||||
|
# Drop the sequence if exists (Django 4.1+ identity columns
|
||||||
|
# don't have it).
|
||||||
self.sql_delete_sequence
|
self.sql_delete_sequence
|
||||||
% {
|
% {
|
||||||
"sequence": self.quote_name(sequence_name),
|
"sequence": self.quote_name(sequence_name),
|
||||||
|
|
|
@ -309,6 +309,9 @@ Models
|
||||||
allows customizing attributes of fields that don't affect a column
|
allows customizing attributes of fields that don't affect a column
|
||||||
definition.
|
definition.
|
||||||
|
|
||||||
|
* On PostgreSQL, ``AutoField``, ``BigAutoField``, and ``SmallAutoField`` are
|
||||||
|
now created as identity columns rather than serial columns with sequences.
|
||||||
|
|
||||||
Requests and Responses
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -27,3 +27,18 @@ class DatabaseSequenceTests(TestCase):
|
||||||
seqs,
|
seqs,
|
||||||
[{"table": Person._meta.db_table, "column": "id", "name": "pers_seq"}],
|
[{"table": Person._meta.db_table, "column": "id", "name": "pers_seq"}],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_get_sequences_old_serial(self):
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("CREATE TABLE testing (serial_field SERIAL);")
|
||||||
|
seqs = connection.introspection.get_sequences(cursor, "testing")
|
||||||
|
self.assertEqual(
|
||||||
|
seqs,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"table": "testing",
|
||||||
|
"column": "serial_field",
|
||||||
|
"name": "testing_serial_field_seq",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
|
@ -1570,11 +1570,7 @@ class SchemaTests(TransactionTestCase):
|
||||||
Author.objects.create(name="Foo")
|
Author.objects.create(name="Foo")
|
||||||
Author.objects.create(name="Bar")
|
Author.objects.create(name="Bar")
|
||||||
|
|
||||||
def test_alter_autofield_pk_to_bigautofield_pk_sequence_owner(self):
|
def test_alter_autofield_pk_to_bigautofield_pk(self):
|
||||||
"""
|
|
||||||
Converting an implicit PK to BigAutoField(primary_key=True) should keep
|
|
||||||
a sequence owner on PostgreSQL.
|
|
||||||
"""
|
|
||||||
with connection.schema_editor() as editor:
|
with connection.schema_editor() as editor:
|
||||||
editor.create_model(Author)
|
editor.create_model(Author)
|
||||||
old_field = Author._meta.get_field("id")
|
old_field = Author._meta.get_field("id")
|
||||||
|
@ -1591,14 +1587,9 @@ class SchemaTests(TransactionTestCase):
|
||||||
)
|
)
|
||||||
if sequence_reset_sqls:
|
if sequence_reset_sqls:
|
||||||
cursor.execute(sequence_reset_sqls[0])
|
cursor.execute(sequence_reset_sqls[0])
|
||||||
# Fail on PostgreSQL if sequence is missing an owner.
|
|
||||||
self.assertIsNotNone(Author.objects.create(name="Bar"))
|
self.assertIsNotNone(Author.objects.create(name="Bar"))
|
||||||
|
|
||||||
def test_alter_autofield_pk_to_smallautofield_pk_sequence_owner(self):
|
def test_alter_autofield_pk_to_smallautofield_pk(self):
|
||||||
"""
|
|
||||||
Converting an implicit PK to SmallAutoField(primary_key=True) should
|
|
||||||
keep a sequence owner on PostgreSQL.
|
|
||||||
"""
|
|
||||||
with connection.schema_editor() as editor:
|
with connection.schema_editor() as editor:
|
||||||
editor.create_model(Author)
|
editor.create_model(Author)
|
||||||
old_field = Author._meta.get_field("id")
|
old_field = Author._meta.get_field("id")
|
||||||
|
@ -1615,7 +1606,6 @@ class SchemaTests(TransactionTestCase):
|
||||||
)
|
)
|
||||||
if sequence_reset_sqls:
|
if sequence_reset_sqls:
|
||||||
cursor.execute(sequence_reset_sqls[0])
|
cursor.execute(sequence_reset_sqls[0])
|
||||||
# Fail on PostgreSQL if sequence is missing an owner.
|
|
||||||
self.assertIsNotNone(Author.objects.create(name="Bar"))
|
self.assertIsNotNone(Author.objects.create(name="Bar"))
|
||||||
|
|
||||||
def test_alter_int_pk_to_autofield_pk(self):
|
def test_alter_int_pk_to_autofield_pk(self):
|
||||||
|
|
Loading…
Reference in New Issue