Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #373 -- Added CompositePrimaryKey-based Meta.primary_key. #18056

Open
wants to merge 72 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
4be1c68
Fixed #373 -- Added CompositeField-based Meta.primary_key.
csirmazbendeguz Apr 7, 2024
cee213d
Fixed #373 -- Add field to error message, simplify tuple operation er…
csirmazbendeguz May 8, 2024
e2d8868
Fixed #373 -- Rename CompositeField to CompositePrimaryKey
csirmazbendeguz May 8, 2024
259605a
Fixed #373 -- Add __len__ to CompositePrimaryKey
csirmazbendeguz May 8, 2024
9e01443
Fixed #373 -- Add some docs
csirmazbendeguz May 9, 2024
fa4af00
Fixed #373 -- Fix docs formatting
csirmazbendeguz May 9, 2024
8354bd0
Fixed #373 -- Add more docs
csirmazbendeguz May 9, 2024
d20f8e0
Fixed #373 -- Fix docs
csirmazbendeguz May 9, 2024
dcd78fc
Fixed #373 -- Fix integrity error tests
csirmazbendeguz May 9, 2024
3576dec
Fixed #373 -- Remove unnecessary asserts
csirmazbendeguz May 9, 2024
1ef76e4
Fixed #373 -- Make a test simpler
csirmazbendeguz May 9, 2024
3d0dcf3
Fixed #373 -- Make get tests better
csirmazbendeguz May 9, 2024
6a2bb92
Fixed #373 -- Fix test
csirmazbendeguz May 9, 2024
a2ec10c
Fixed #373 -- Address create tests comments
csirmazbendeguz May 10, 2024
3071ad9
Fixed #373 -- Address delete tests comments
csirmazbendeguz May 10, 2024
b7179b6
Fixed #373 -- Address filter tests comments
csirmazbendeguz May 10, 2024
cb4f300
Fixed #373 -- Add test for ordering comments
csirmazbendeguz May 10, 2024
6922337
Fixed #373 -- Fix ordering tes
csirmazbendeguz May 10, 2024
b7b9580
Fixed #373 - Simplify tests
csirmazbendeguz May 11, 2024
9b17831
Fixed #373 - Simplify tests
csirmazbendeguz May 11, 2024
6285cff
Fixed #373 - Don't check for CompositePrimaryKey directly in SQL comp…
csirmazbendeguz May 11, 2024
22d6c9d
Fixed #373 - Simplify statement
csirmazbendeguz May 11, 2024
ad51da4
Fixed #373 - Use MultiColSource instead of Cols
csirmazbendeguz May 11, 2024
9c9e57d
Fixed #373 - Add system checks
csirmazbendeguz May 13, 2024
c8f796e
Fixed #373 - Add CompositeValue class
csirmazbendeguz May 13, 2024
26fb669
Fixed #373 - Fix test
csirmazbendeguz May 14, 2024
87b1a23
Fixed #373 - Test related names
csirmazbendeguz May 14, 2024
389daa9
Fixed #373 - Add test for composite pk attnames
csirmazbendeguz May 14, 2024
2515066
Fixed #373 - Add test for composite pk state
csirmazbendeguz May 14, 2024
350c58d
Fixed #373 - Fix error message
csirmazbendeguz May 16, 2024
220b3ed
Fixed #373 - Simplify checking if pk is set
csirmazbendeguz May 16, 2024
b9a8143
Fixed #373 - Fix error message
csirmazbendeguz May 16, 2024
141b27d
Fixed #373 - Fix error message
csirmazbendeguz May 16, 2024
926822f
Fixed #373 - Add explanatory comment
csirmazbendeguz May 16, 2024
ac29218
Fixed #373 - Add explanatory comment
csirmazbendeguz May 16, 2024
0b194eb
Fixed #373 - Minor refactoring in test
csirmazbendeguz May 16, 2024
b6727d2
Fixed #373 - Small refactor for select mask
csirmazbendeguz May 16, 2024
21d48d5
Fixed #373 - Revert auto field changes
csirmazbendeguz May 16, 2024
b839b2b
Fixed #373 - A minor improvement to sql update compiler
csirmazbendeguz May 16, 2024
006736c
Fixed #373 - Fix source expressions
csirmazbendeguz May 16, 2024
3bf4004
Fixed #373 - Simplify error messages
csirmazbendeguz May 16, 2024
90a8d43
Fixed #373 - Fix linter error
csirmazbendeguz May 16, 2024
7bc3f40
Fixed #373 - Fix set source expressions
csirmazbendeguz May 17, 2024
f4db656
Fixed #373 - Don't ignore params in as_sql
csirmazbendeguz May 17, 2024
e7deac6
Fixed #373 - Fix ordering issue
csirmazbendeguz May 17, 2024
e15f55d
Fixed #373 - Revert using MultiColSource
csirmazbendeguz May 19, 2024
79bf5e8
Fixed #373 - Implement gt, gte lookups for tuples
csirmazbendeguz May 19, 2024
92f2807
Fixed #373 - Implement lt, lte lookups for tuples
csirmazbendeguz May 19, 2024
c5c5239
Fixed #373 - Refactor in lookup
csirmazbendeguz May 20, 2024
8e408b6
Fixed #373 - Refactor tests
csirmazbendeguz May 20, 2024
69143d9
Fixed #373 - Improve error messages
csirmazbendeguz May 20, 2024
586434c
Fixed #373 - Fix subTest message
csirmazbendeguz May 20, 2024
0478d46
Fixed #373 - Add lookup error tests
csirmazbendeguz May 21, 2024
23e0e0b
Fixed #373 - Add null check for primary key fields
csirmazbendeguz May 21, 2024
a9ef232
Fixed #373 - Raise value error instead of check error for Meta.primar…
csirmazbendeguz May 22, 2024
694fcc8
Fixed #373 - Add test for checking foreign object's unique target
csirmazbendeguz May 22, 2024
b49ea71
Fixed #373 - Simplify checks
csirmazbendeguz May 23, 2024
9fbb766
Fixed #373 - Add pk_fields to Options
csirmazbendeguz May 23, 2024
96156dd
Fixed #373 - Adjust indentation
csirmazbendeguz May 23, 2024
13c0053
Fixed #373 - Remove try catch checking pk_set
csirmazbendeguz May 23, 2024
9ce6781
Fixed #373 - Remove try catch from function
csirmazbendeguz May 23, 2024
2f24f77
Fixed #373 - Rename to _unnest_composite_fields
csirmazbendeguz May 23, 2024
2c8d38c
Fixed #373 - Refactor is_set
csirmazbendeguz May 23, 2024
cff46d5
Fixed #373 - Add docs
csirmazbendeguz May 23, 2024
39a2a09
Fixed #373 - Add duplicate pk check
csirmazbendeguz May 23, 2024
2478e14
Fixed #373 - Throw ValueError for duplicates instead of check error
csirmazbendeguz May 23, 2024
76191b5
Fixed #373 - Add docs about migration
csirmazbendeguz May 23, 2024
208a457
Fixed #373 - Refactor tuple lookups to classes
csirmazbendeguz May 23, 2024
b74f3ef
Fixed #373 - Revert new line
csirmazbendeguz May 23, 2024
70fd4be
Fixed #373 - Make variable names more consistent
csirmazbendeguz May 23, 2024
4caee01
Fixed #373 - Add some comments to lookups
csirmazbendeguz May 23, 2024
272184d
Fixed #373 - Add validation error to clean
csirmazbendeguz May 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions django/db/backends/base/schema.py
Expand Up @@ -105,6 +105,7 @@ class BaseDatabaseSchemaEditor:
sql_check_constraint = "CHECK (%(check)s)"
sql_delete_constraint = "ALTER TABLE %(table)s DROP CONSTRAINT %(name)s"
sql_constraint = "CONSTRAINT %(name)s %(constraint)s"
sql_pk_constraint = "PRIMARY KEY (%(columns)s)"

sql_create_check = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s CHECK (%(check)s)"
sql_delete_check = sql_delete_constraint
Expand Down Expand Up @@ -268,6 +269,13 @@ def table_sql(self, model):
constraint.constraint_sql(model, self)
for constraint in model._meta.constraints
]

# If the model defines Meta.primary_key, add the primary key constraint
# to the table definition.
# It's expected primary_key=True isn't set on any fields (see E043).
if model._meta.primary_key:
constraints.append(self._pk_constraint_sql(model._meta.primary_key))

sql = self.sql_create_table % {
"table": self.quote_name(model._meta.db_table),
"definition": ", ".join(
Expand Down Expand Up @@ -1967,6 +1975,11 @@ def _constraint_names(
result.append(name)
return result

def _pk_constraint_sql(self, fields):
return self.sql_pk_constraint % {
"columns": ", ".join(self.quote_name(field) for field in fields)
}

def _delete_primary_key(self, model, strict=False):
constraint_names = self._constraint_names(model, primary_key=True)
if strict and len(constraint_names) != 1:
Expand Down
72 changes: 70 additions & 2 deletions django/db/models/base.py
Expand Up @@ -1080,7 +1080,7 @@ def _save_table(
if pk_val is None:
pk_val = meta.pk.get_pk_value_on_save(self)
setattr(self, meta.pk.attname, pk_val)
pk_set = pk_val is not None
csirmazbendeguz marked this conversation as resolved.
Show resolved Hide resolved
pk_set = meta.pk.is_set(pk_val)
if not pk_set and (force_update or update_fields):
raise ValueError("Cannot force an update in save() with no primary key.")
updated = False
Expand Down Expand Up @@ -1369,10 +1369,12 @@ def _get_unique_checks(self, exclude=None, include_meta_constraints=False):
"""
if exclude is None:
exclude = set()
unique_checks = []

unique_checks = []
unique_togethers = [(self.__class__, self._meta.unique_together)]
constraints = []
pk_fields = self._meta.pk_fields

if include_meta_constraints:
constraints = [(self.__class__, self._meta.total_unique_constraints)]
for parent_class in self._meta.all_parents:
Expand All @@ -1385,6 +1387,16 @@ def _get_unique_checks(self, exclude=None, include_meta_constraints=False):
(parent_class, parent_class._meta.total_unique_constraints)
)

if (
self._meta.primary_key
and "primary_key" not in exclude
and "pk" not in exclude
and not any(field.name in exclude for field in pk_fields)
):
unique_checks.append(
(self.__class__, tuple(field.name for field in pk_fields))
)

for model_class, unique_together in unique_togethers:
for check in unique_together:
if not any(name in exclude for name in check):
Expand Down Expand Up @@ -1420,6 +1432,7 @@ def _get_unique_checks(self, exclude=None, include_meta_constraints=False):
date_checks.append((model_class, "year", name, f.unique_for_year))
if f.unique_for_month and f.unique_for_month not in exclude:
date_checks.append((model_class, "month", name, f.unique_for_month))

return unique_checks, date_checks

def _perform_unique_checks(self, unique_checks):
Expand Down Expand Up @@ -1686,6 +1699,7 @@ def check(cls, **kwargs):
*cls._check_constraints(databases),
*cls._check_default_pk(),
*cls._check_db_table_comment(databases),
*cls._check_composite_pk(),
]

return errors
Expand All @@ -1694,6 +1708,9 @@ def check(cls, **kwargs):
def _check_default_pk(cls):
if (
not cls._meta.abstract
# If the model defines Meta.primary_key, the check should be skipped,
# since there's no default primary key.
and not cls._meta.primary_key
and cls._meta.pk.auto_created
and
# Inherited PKs are checked in parents models.
Expand Down Expand Up @@ -1722,6 +1739,52 @@ def _check_default_pk(cls):
]
return []

@classmethod
def _check_composite_pk(cls):
errors = []
meta = cls._meta

if meta.primary_key is None:
return errors

for field in meta.fields:
if field.primary_key:
errors.append(
checks.Error(
"'%s' may not set 'primary_key=True' if 'Meta.primary_key' "
"is defined." % (field.name,),
obj=cls,
id="models.E042",
)
)

fields_map = {field.attname: field for field in meta.concrete_fields}

for field_name in meta.primary_key:
field = fields_map.get(field_name)
if not field:
errors.append(
checks.Error(
"'%s' cannot be included in 'Meta.primary_key'."
% (field_name,),
hint="'%s' is not an existing, concrete field." % (field_name,),
obj=cls,
id="models.E043",
)
)
elif field.null:
errors.append(
checks.Error(
"'%s' cannot be included in 'Meta.primary_key'."
% (field.name,),
hint="'%s' may not set 'null=True'." % (field.name,),
obj=cls,
id="models.E043",
)
)

return errors

@classmethod
def _check_db_table_comment(cls, databases):
if not cls._meta.db_table_comment:
Expand Down Expand Up @@ -1842,6 +1905,11 @@ def _check_m2m_through_same_relationship(cls):
@classmethod
def _check_id_field(cls):
"""Check if `id` field is a primary key."""
# If the model defines Meta.primary_key, the check should be skipped,
# since primary_key=True can't be set on any fields (including `id`).
if cls._meta.primary_key:
return []

fields = [
f for f in cls._meta.local_fields if f.name == "id" and f != cls._meta.pk
]
Expand Down
28 changes: 28 additions & 0 deletions django/db/models/expressions.py
Expand Up @@ -1274,6 +1274,34 @@ def get_db_converters(self, connection):
) + self.target.get_db_converters(connection)


class Cols(Expression):
def __init__(self, alias, targets, output_field):
super().__init__(output_field=output_field)
self.targets, self.alias = targets, alias

def __len__(self):
return len(self.targets)

def get_source_expressions(self):
return [Col(self.alias, target) for target in self.targets]

def set_source_expressions(self, exprs):
assert all(isinstance(expr, Col) for expr in exprs)
self.targets = [col.target for col in exprs]

def as_sql(self, compiler, connection):
cols_sql = []
cols_params = []
cols = self.get_source_expressions()

for col in cols:
sql, params = col.as_sql(compiler, connection)
cols_sql.append(sql)
cols_params.extend(params)

return ", ".join(cols_sql), cols_params


class Ref(Expression):
"""
Reference to column alias of the query. For example, Ref('sum_cost') in
Expand Down
4 changes: 4 additions & 0 deletions django/db/models/fields/__init__.py
Expand Up @@ -1153,6 +1153,10 @@ def slice_expression(self, expression, start, length):
"""Return a slice of this field."""
raise NotSupportedError("This field does not support slicing.")

@classmethod
def is_set(cls, value):
return value is not None


class BooleanField(Field):
empty_strings_allowed = False
Expand Down