diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py index b7e35760cb..d17dc8dcce 100644 --- a/django/db/backends/base/operations.py +++ b/django/db/backends/base/operations.py @@ -311,7 +311,7 @@ class BaseDatabaseOperations: """ return value - def return_insert_id(self): + def return_insert_id(self, field): """ For backends that support returning the last insert ID as part of an insert query, return the SQL and params to append to the INSERT query. diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index 6669ca5beb..77c568e84b 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -12,7 +12,7 @@ from django.utils.encoding import force_bytes, force_str from django.utils.functional import cached_property from .base import Database -from .utils import BulkInsertMapper, InsertIdVar, Oracle_datetime +from .utils import BulkInsertMapper, InsertVar, Oracle_datetime class DatabaseOperations(BaseDatabaseOperations): @@ -333,8 +333,8 @@ END; match_option = "'i'" return 'REGEXP_LIKE(%%s, %%s, %s)' % match_option - def return_insert_id(self): - return "RETURNING %s INTO %%s", (InsertIdVar(),) + def return_insert_id(self, field): + return 'RETURNING %s INTO %%s', (InsertVar(field),) def __foreign_key_constraints(self, table_name, recursive): with self.connection.cursor() as cursor: diff --git a/django/db/backends/oracle/utils.py b/django/db/backends/oracle/utils.py index 02e3f754a8..64e69136bf 100644 --- a/django/db/backends/oracle/utils.py +++ b/django/db/backends/oracle/utils.py @@ -3,14 +3,26 @@ import datetime from .base import Database -class InsertIdVar: +class InsertVar: """ A late-binding cursor variable that can be passed to Cursor.execute as a parameter, in order to receive the id of the row created by an insert statement. """ + types = { + 'FloatField': Database.NATIVE_FLOAT, + 'CharField': str, + 'DateTimeField': Database.TIMESTAMP, + 'DateField': Database.DATETIME, + 'DecimalField': Database.NUMBER, + } + + def __init__(self, field): + internal_type = getattr(field, 'target_field', field).get_internal_type() + self.db_type = self.types.get(internal_type, int) + def bind_parameter(self, cursor): - param = cursor.cursor.var(int) + param = cursor.cursor.var(self.db_type) cursor._insert_id_var = param return param diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py index c502760e93..0c497edf71 100644 --- a/django/db/backends/postgresql/operations.py +++ b/django/db/backends/postgresql/operations.py @@ -235,7 +235,7 @@ class DatabaseOperations(BaseDatabaseOperations): return cursor.query.decode() return None - def return_insert_id(self): + def return_insert_id(self, field): return "RETURNING %s", () def bulk_insert_sql(self, fields, placeholder_rows): diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index a14f1254aa..eaccc96d7d 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1304,7 +1304,7 @@ class SQLInsertCompiler(SQLCompiler): if ignore_conflicts_suffix_sql: result.append(ignore_conflicts_suffix_sql) col = "%s.%s" % (qn(opts.db_table), qn(opts.pk.column)) - r_fmt, r_params = self.connection.ops.return_insert_id() + r_fmt, r_params = self.connection.ops.return_insert_id(opts.pk) # Skip empty r_fmt to allow subclasses to customize behavior for # 3rd party backends. Refs #19096. if r_fmt: diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index c7f1a4e78b..6c25f79d7b 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -353,6 +353,9 @@ backends. :class:`~django.db.models.DateTimeField` in ``datetime_cast_date_sql()``, ``datetime_extract_sql()``, etc. +* ``DatabaseOperations.return_insert_id()`` now requires an additional + ``field`` argument with the model field. + :mod:`django.contrib.admin` --------------------------- diff --git a/tests/backends/models.py b/tests/backends/models.py index 1fa8d44e63..a2c8616cc6 100644 --- a/tests/backends/models.py +++ b/tests/backends/models.py @@ -5,6 +5,10 @@ from django.contrib.contenttypes.models import ContentType from django.db import models +class NonIntegerAutoField(models.Model): + creation_datetime = models.DateTimeField(primary_key=True) + + class Square(models.Model): root = models.IntegerField() square = models.PositiveIntegerField() diff --git a/tests/backends/oracle/tests.py b/tests/backends/oracle/tests.py index a0d49854d9..30d981da69 100644 --- a/tests/backends/oracle/tests.py +++ b/tests/backends/oracle/tests.py @@ -1,3 +1,4 @@ +import datetime import unittest from django.db import connection @@ -5,7 +6,7 @@ from django.db.models.fields import BooleanField, NullBooleanField from django.db.utils import DatabaseError from django.test import TransactionTestCase -from ..models import Square +from ..models import NonIntegerAutoField, Square @unittest.skipUnless(connection.vendor == 'oracle', 'Oracle tests') @@ -95,3 +96,23 @@ class TransactionalTests(TransactionTestCase): self.assertIn('ORA-01017', context.exception.args[0].message) finally: connection.settings_dict['PASSWORD'] = old_password + + def test_non_integer_auto_field(self): + with connection.cursor() as cursor: + # Create trigger that fill non-integer auto field. + cursor.execute(""" + CREATE OR REPLACE TRIGGER "TRG_FILL_CREATION_DATETIME" + BEFORE INSERT ON "BACKENDS_NONINTEGERAUTOFIELD" + FOR EACH ROW + BEGIN + :NEW.CREATION_DATETIME := SYSTIMESTAMP; + END; + """) + try: + NonIntegerAutoField._meta.auto_field = NonIntegerAutoField.creation_datetime + obj = NonIntegerAutoField.objects.create() + self.assertIsNotNone(obj.creation_datetime) + self.assertIsInstance(obj.creation_datetime, datetime.datetime) + finally: + with connection.cursor() as cursor: + cursor.execute('DROP TRIGGER "TRG_FILL_CREATION_DATETIME"')