Skip to content

Improve support for non-integer primary keys#279

Open
knokko wants to merge 5 commits into
masterfrom
non-integer-ids
Open

Improve support for non-integer primary keys#279
knokko wants to merge 5 commits into
masterfrom
non-integer-ids

Conversation

@knokko
Copy link
Copy Markdown

@knokko knokko commented May 7, 2026

This PR improves support for non-integer primary keys

Currently, binder sort of allows it, but:

  • You cannot invoke detail endpoints
  • You cannot invoke detail routes
  • The name of the primary key field is always sent as id over the network,
    regardless of what the name of the primary key field is.
  • Any relations will fail miserably because binder accepts only integer IDs.

This PR fixes all of these shortcomings.
Furthermore, it fixes some history changeset inconsistencies.

@knokko knokko force-pushed the non-integer-ids branch from 3871f4e to cce13d7 Compare May 8, 2026 10:26
@BBooijLiewes BBooijLiewes self-requested a review May 8, 2026 13:13
@knokko
Copy link
Copy Markdown
Author

knokko commented May 8, 2026

Note that filtering on foreign key is now handled by this code:

# For foreign keys and similar fields, we should use the filter of the primary key of the related object
class_for_filtering = field.__class__
if isinstance(field, models.ForeignObject) or isinstance(field, models.ManyToManyField) or isinstance(field, models.ForeignObjectRel):
	class_for_filtering = field.related_model._meta.pk.__class__

Prior to this PR, it was hardcoded as part of IntegerField.

@knokko knokko force-pushed the non-integer-ids branch 10 times, most recently from de3d364 to ce5801c Compare May 28, 2026 15:29
@knokko knokko changed the title wip: non-integer primary keys Improve support for non-integer primary keys Jun 4, 2026
@knokko knokko marked this pull request as ready for review June 4, 2026 07:25
Copy link
Copy Markdown
Contributor

@BBooijLiewes BBooijLiewes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets also test this in a few internal project pipelines before merging.

Comment thread binder/history.py
before = model_class.format_field_for_history(field_name=c.field, raw_value=c.before, is_before=True, diff_tracker=diff_tracker, oid=oid)
changes.append({'model': c.model, 'oid': c.oid, 'field': c.field, 'diff': c.diff, 'before': before, 'after': after})
data.append({'date': cs.date, 'uuid': cs.uuid, 'id': cs.id, 'source': cs.source, 'user': cs.user_id, 'changes': changes})
data.append({'date': cs.date, 'uuid': cs.uuid, 'id': cs.pk, 'source': cs.source, 'user': cs.user_id, 'changes': changes})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check serialization compisite PK, maybe add test case?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just added a fix + test case for string (non-integer) primary keys.

I also tried to add a test case for composite primary keys, but this is currently impossible:

  • Binder currently doesn't support detail endpoints for models with composite pk's, so I can't use /api/model_with_composite_pk/pk_tuple/history/.
  • Django doesn't support forward relations to models with composite pk's
  • Our history endpoint ignores backward relations

Comment thread binder/models.py


class BinderModel(models.Model, metaclass=BinderModelBase):
pk_regex = '[0-9]+'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that PK's can be composite does this still hold? Or should we add check in places?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, this PR does not support detail endpoints for models with composite primary keys. For instance, if I create an OrderAddition model with a composite primary key (order_id, addition_number), there is currently no way to address it. What would it even look like? /api/order_addition/(1234,5)/?

Also note that this pk_regex = '[0-9]+' is the default regex, which models with more complicated primary keys can override.

Comment thread binder/models.py
Comment on lines +573 to +574
if isinstance(entry, list) and len(entry) == 1:
entry = entry[0]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment what this does

Comment on lines +9 to +10
else:
return models.CompositePrimaryKey('contact_person', 'date')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also build a check in binder itself if you try to use a composite key on a binder model with binder < 5 we give a warning on migration?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you mean django < 5 rather than binder < 5.

Django < 5 simply doesn't support CompositePrimaryKey, so it will already give an error.

If you use a CompositePrimaryKey on django 5 but with an old binder version, django won't give any errors, but binder will fail miserably. I could add a helpful error message, but older versions of binder wouldn't have that helpful error message, which makes it useless.

Comment thread binder/views.py Outdated
Comment on lines +713 to +714
# I would like to remove the next line completely, but that might break downstream projects
data['id'] = data[self.model._meta.pk.name]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No comment in first person

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better now?

Comment thread binder/views.py Outdated
queryset = queryset.annotate(**{name: annotations[name] for name in required_annotations})
try:
obj = queryset.get(pk=int(after_id))
obj = queryset.get(pk=int(after_id)) # TODO Check this?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check ToDo @knokko

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better now?

Comment thread binder/views.py


def _is_classic_id(self, pk):
return (pk.name == 'id' and isinstance(pk, models.IntegerField)) or (pk.name.endswith('_ptr') and isinstance(pk, models.ForeignObject))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think modern djanbo uses a big int field by default.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread binder/views.py Outdated
continue

if isinstance(data[field.name], int):
if isinstance(data[field.name], int): # TODO Check right type?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToDo

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better?

Comment thread binder/views.py Outdated
elif isinstance(data[field.name], list):
for i, mid in enumerate(data[field.name]):
if isinstance(mid, int):
if isinstance(mid, int): # TODO Check right type?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToDo

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better?

Comment thread binder/views.py Outdated
raise BinderRequestError('with.{}.{} should be a list'.format(model.__name__, field.name))
for i, v in enumerate(values[field.name]):
if not isinstance(multiput_get_id(v), int):
if not isinstance(multiput_get_id(v), int): # TODO Check right type
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToDo

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better?

@knokko knokko force-pushed the non-integer-ids branch 5 times, most recently from ac444e6 to c639c89 Compare June 4, 2026 13:23
knokko added 4 commits June 4, 2026 16:01
This commit dramatically improves the way binder handles non-integer primary keys.

Currently, binder sort of allows it, but:
- You cannot invoke detail endpoints
- You cannot invoke detail routes
- You cannot invoke the history route
- You cannot create, modify, or delete such objects
- The name of the primary key field is always sent as `id` over the network,
regardless of what the name of the primary key field is.
- Any relations will fail miserably because binder accepts only integer IDs.

This commit fixes all of these shortcomings.
Before updating django-hijack, the /user/endmasquerade/ endpoint would yield 200 when it succeeds,
and yield 403 when it fails because the user is not masquerated.

After updating django-hijack, it would always yield 200.

This commit restores the old behavior of returning 403 for non-masquerated users.
This commit improves our support for `models.CompositePrimaryKey` by properly handling
models with `CompositePrimaryKey`s in the `with`s of other objects.
(Prior to this commit, it would fail miserably.)
@knokko knokko force-pushed the non-integer-ids branch from e813ec7 to 969f990 Compare June 4, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants