From 518414f4b364980d7708daaeed04077a3aaad1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Kr=C3=B6nke?= Date: Sat, 25 May 2024 12:32:58 +0200 Subject: [PATCH] offer MYSQL_COMPATIBILITY setting to remove unique from the model (#267) * offer MYSQL_COMPATIBILITY setting to remove unique from the model * Update 0010_unique_registration_id.py * Update 0011_fcmdevice_fcm_django_registration_id_user_id_idx.py * black * explain edge case and document alternative to get the best of both worlds * add missing new line * improve section order --- docs/index.rst | 46 ++++++++++++++++--- .../migrations/0010_unique_registration_id.py | 24 ++++++---- ..._fcm_django_registration_id_user_id_idx.py | 22 +++++---- fcm_django/models.py | 9 ++-- fcm_django/settings.py | 3 ++ 5 files changed, 77 insertions(+), 27 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 6d5f3450..7cfef946 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -422,7 +422,7 @@ If you don't want default table appears in the DB then you should remove ``fcm_d After setup your own ``Model`` don't forget to create ``migrations`` for your app and call ``migrate`` command. -After removing ``"fcm_django"`` out of ``INSTALLED_APPS``. You will need to re-register the Device in order to see it in the admin panel. +After removing ``"fcm_django"`` out of ``INSTALLED_APPS``. You will need to re-register the Device in order to see it in the admin panel. This can be accomplished as follows at ``your_app/admin.py``: .. code-block:: python @@ -442,12 +442,46 @@ If you choose to move forward with swapped models then: 1. On existed project you have to keep in mind there are required manual work to move data from one table to anther. 2. If there's any tables with FK to swapped model then you have to deal with them on your own. -Note: This functionality based on `Swapper `_ that based on functionality +Note: This functionality based on `Swapper `_ that based on functionality that allow to use a `custom User model `_. -So this functionality have the same limitations. -The most is important limitation it is that is difficult to start out with a default (non-swapped) model +So this functionality have the same limitations. +The most is important limitation it is that is difficult to start out with a default (non-swapped) model and then later to switch to a swapped implementation without doing some migration hacking. +MySQL compatibility +------------------- +MySQL has a limit for indices and therefore the `registration_id` field cannot be made unique in MySQL. +We detect the database backend and remove the unique constraint for MySQL in the migration files. However, +to ensure that the constraint is removed from the actual model you have to add the following to your settings +to be able to run your django tests with MySQL and without running all migrations: + +.. code-block:: python + + FCM_DJANGO_SETTINGS = { + "MYSQL_COMPATIBILITY": True, + # [...] your other settings + } + +As an alternative, you can use a custom model (see above) and either remove the unique constraint manually +or use a length limited CharField for the `registration_id` field. There are no guarantees on the max length of +FCM tokens, but in practice they are less than 200 characters long. Therefore, a CharField with a length of 600 +should be sufficient and you can make it unique and index it even with MySQL: + +.. code-block:: python + + from fcm_django.models import AbstractFCMDevice, FCMDevice as OriginalFCMDevice + + + class CustomFCMDevice(AbstractFCMDevice): + registration_id = models.CharField( + verbose_name="Registration token", + unique=True, + max_length=600, # https://stackoverflow.com/a/64902685 better to be safe than sorry + ) + + class Meta(OriginalFCMDevice.Meta): + pass + Python 3 support ---------------- - ``fcm-django`` is fully compatible with Python 3.7+ @@ -481,11 +515,11 @@ Because there's possibility to use swapped models therefore tests contains two c 1. with default settings and non swapped models ``settings/default.py`` 2. and with overwritten settings only that required by swapper - ``settings/swap.py`` -To run tests locally you could use ``pytest``, and if you need to check migrations on different DB then you have to specify environment variable ``DATABASE_URL`` ie +To run tests locally you could use ``pytest``, and if you need to check migrations on different DB then you have to specify environment variable ``DATABASE_URL`` ie .. code-block:: console export DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/postgres - export DJANGO_SETTINGS_MODULE=tests.settings.default + export DJANGO_SETTINGS_MODULE=tests.settings.default # or export DJANGO_SETTINGS_MODULE=tests.settings.swap pytest diff --git a/fcm_django/migrations/0010_unique_registration_id.py b/fcm_django/migrations/0010_unique_registration_id.py index dee1bc18..671d1a1b 100644 --- a/fcm_django/migrations/0010_unique_registration_id.py +++ b/fcm_django/migrations/0010_unique_registration_id.py @@ -2,6 +2,8 @@ from django.db import migrations, models +from fcm_django.settings import FCM_DJANGO_SETTINGS as SETTINGS + _MYSQL = "mysql" @@ -33,13 +35,17 @@ class Migration(migrations.Migration): ("fcm_django", "0009_alter_fcmdevice_user"), ] - operations = [ - AlterFieldSkipMySQL( - model_name="fcmdevice", - name="registration_id", - field=models.TextField( - verbose_name="Registration token", - unique=True, + operations = ( + [ + AlterFieldSkipMySQL( + model_name="fcmdevice", + name="registration_id", + field=models.TextField( + verbose_name="Registration token", + unique=True, + ), ), - ), - ] + ] + if not SETTINGS["MYSQL_COMPATIBILITY"] + else [] + ) diff --git a/fcm_django/migrations/0011_fcmdevice_fcm_django_registration_id_user_id_idx.py b/fcm_django/migrations/0011_fcmdevice_fcm_django_registration_id_user_id_idx.py index eb9ce9aa..fc447ea0 100644 --- a/fcm_django/migrations/0011_fcmdevice_fcm_django_registration_id_user_id_idx.py +++ b/fcm_django/migrations/0011_fcmdevice_fcm_django_registration_id_user_id_idx.py @@ -2,6 +2,8 @@ from django.db import migrations, models +from fcm_django.settings import FCM_DJANGO_SETTINGS as SETTINGS + _MYSQL = "mysql" @@ -33,12 +35,16 @@ class Migration(migrations.Migration): ("fcm_django", "0010_unique_registration_id"), ] - operations = [ - AddIndexSkipMySQL( - model_name="fcmdevice", - index=models.Index( - fields=["registration_id", "user"], - name="fcm_django__registr_dacdb2_idx", + operations = ( + [ + AddIndexSkipMySQL( + model_name="fcmdevice", + index=models.Index( + fields=["registration_id", "user"], + name="fcm_django__registr_dacdb2_idx", + ), ), - ), - ] + ] + if not SETTINGS["MYSQL_COMPATIBILITY"] + else [] + ) diff --git a/fcm_django/models.py b/fcm_django/models.py index 9a0038c5..a1bba7f4 100644 --- a/fcm_django/models.py +++ b/fcm_django/models.py @@ -278,7 +278,7 @@ class AbstractFCMDevice(Device): ) registration_id = models.TextField( verbose_name=_("Registration token"), - unique=True, + unique=not SETTINGS["MYSQL_COMPATIBILITY"], ) type = models.CharField(choices=DeviceType.choices, max_length=10) objects: "FCMDeviceQuerySet" = FCMDeviceManager() @@ -390,9 +390,10 @@ class Meta: verbose_name = _("FCM device") verbose_name_plural = _("FCM devices") - indexes = [ - models.Index(fields=["registration_id", "user"]), - ] + if not SETTINGS["MYSQL_COMPATIBILITY"]: + indexes = [ + models.Index(fields=["registration_id", "user"]), + ] app_label = "fcm_django" swappable = swapper.swappable_setting("fcm_django", "fcmdevice") diff --git a/fcm_django/settings.py b/fcm_django/settings.py index 95d9ef60..465f4661 100644 --- a/fcm_django/settings.py +++ b/fcm_django/settings.py @@ -24,3 +24,6 @@ "invalid_package_name": "InvalidPackageName", }, ) + +# MySQL compatibility +FCM_DJANGO_SETTINGS.setdefault("MYSQL_COMPATIBILITY", False)