import pytest from django.apps import apps from django.db import models from django.db.migrations import AddField, AlterField, RemoveField from django.db.migrations.state import ProjectState from psqlextra.backend.migrations import operations, postgres_patched_migrations from psqlextra.models import ( PostgresMaterializedViewModel, PostgresPartitionedModel, PostgresViewModel, ) from psqlextra.types import PostgresPartitioningMethod from .fake_model import ( define_fake_materialized_view_model, define_fake_partitioned_model, define_fake_view_model, get_fake_model, ) from .migrations import apply_migration, make_migration @pytest.mark.parametrize( "model_config", [ dict( fields={"category": models.TextField()}, partitioning_options=dict( method=PostgresPartitioningMethod.LIST, key="category" ), ), dict( fields={"timestamp": models.DateTimeField()}, partitioning_options=dict( method=PostgresPartitioningMethod.RANGE, key="timestamp" ), ), ], ) @postgres_patched_migrations() def test_make_migration_create_partitioned_model(fake_app, model_config): """Tests whether the right operations are generated when creating a new partitioned model.""" model = define_fake_partitioned_model( **model_config, meta_options=dict(app_label=fake_app.name) ) migration = make_migration(model._meta.app_label) ops = migration.operations # should have one operation to create the partitioned model # and one more to add a default partition assert len(ops) == 2 assert isinstance(ops[0], operations.PostgresCreatePartitionedModel) assert isinstance(ops[1], operations.PostgresAddDefaultPartition) # make sure the base is set correctly assert len(ops[0].bases) == 1 assert issubclass(ops[0].bases[0], PostgresPartitionedModel) # make sure the partitioning options got copied correctly assert ops[0].partitioning_options == model_config["partitioning_options"] # make sure the default partition is named "default" assert ops[1].model_name == model.__name__ assert ops[1].name == "default" @postgres_patched_migrations() def test_make_migration_create_view_model(fake_app): """Tests whether the right operations are generated when creating a new view model.""" underlying_model = get_fake_model({"name": models.TextField()}) model = define_fake_view_model( fields={"name": models.TextField()}, view_options=dict(query=underlying_model.objects.all()), meta_options=dict(app_label=fake_app.name), ) migration = make_migration(model._meta.app_label) ops = migration.operations assert len(ops) == 1 assert isinstance(ops[0], operations.PostgresCreateViewModel) # make sure the base is set correctly assert len(ops[0].bases) == 1 assert issubclass(ops[0].bases[0], PostgresViewModel) # make sure the view options got copied correctly assert ops[0].view_options == model._view_meta.original_attrs @postgres_patched_migrations() def test_make_migration_create_materialized_view_model(fake_app): """Tests whether the right operations are generated when creating a new materialized view model.""" underlying_model = get_fake_model({"name": models.TextField()}) model = define_fake_materialized_view_model( fields={"name": models.TextField()}, view_options=dict(query=underlying_model.objects.all()), meta_options=dict(app_label=fake_app.name), ) migration = make_migration(model._meta.app_label) ops = migration.operations assert len(ops) == 1 assert isinstance(ops[0], operations.PostgresCreateMaterializedViewModel) # make sure the base is set correctly assert len(ops[0].bases) == 1 assert issubclass(ops[0].bases[0], PostgresMaterializedViewModel) # make sure the view options got copied correctly assert ops[0].view_options == model._view_meta.original_attrs @pytest.mark.parametrize( "define_view_model", [define_fake_materialized_view_model, define_fake_view_model], ) @postgres_patched_migrations() def test_make_migration_field_operations_view_models( fake_app, define_view_model ): """Tests whether field operations against a (materialized) view are always wrapped in the :see:ApplyState operation so that they don't actually get applied to the database, yet Django applies to them to the project state. This is important because you can't actually alter/add or delete fields from a (materialized) view. """ underlying_model = get_fake_model( {"first_name": models.TextField(), "last_name": models.TextField()}, meta_options=dict(app_label=fake_app.name), ) model = define_view_model( fields={"first_name": models.TextField()}, view_options=dict(query=underlying_model.objects.all()), meta_options=dict(app_label=fake_app.name), ) state_1 = ProjectState.from_apps(apps) migration = make_migration(model._meta.app_label) apply_migration(migration.operations, state_1) # add a field to the materialized view last_name_field = models.TextField(null=True) last_name_field.contribute_to_class(model, "last_name") migration = make_migration(model._meta.app_label, from_state=state_1) assert len(migration.operations) == 1 assert isinstance(migration.operations[0], operations.ApplyState) assert isinstance(migration.operations[0].state_operation, AddField) # alter the field on the materialized view state_2 = ProjectState.from_apps(apps) last_name_field = models.TextField(null=True, blank=True) last_name_field.contribute_to_class(model, "last_name") migration = make_migration(model._meta.app_label, from_state=state_2) assert len(migration.operations) == 1 assert isinstance(migration.operations[0], operations.ApplyState) assert isinstance(migration.operations[0].state_operation, AlterField) # remove the field from the materialized view migration = make_migration( model._meta.app_label, from_state=ProjectState.from_apps(apps), to_state=state_1, ) assert isinstance(migration.operations[0], operations.ApplyState) assert isinstance(migration.operations[0].state_operation, RemoveField)