Typorithm · easy

Models and database-enforced invariants

pythondjangoindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Models and database-enforced invariants

0%

Concept and contract

Distinguish model field metadata from a constraint that the database actually enforces.

A model describes an application’s persisted objects, but not every model option becomes a database invariant. Choices and validation methods can guide forms without automatically validating every save call.

This lesson deliberately creates rows directly and relies on a CheckConstraint to enforce its small country set. The companion creates the model table in an owned in-memory database; it never edits a real project schema or existing production table.

Mechanism and worked example

The model has ordinary text fields plus a named constraint requiring an allowed country.

The model has ordinary text fields plus a named constraint requiring an allowed country. The helper clears only its fictional fixture rows, attempts one insert inside atomic and converts an IntegrityError into an explicit rejected result.

The count is inspected after leaving the failed transaction block. That placement matters because querying inside a transaction already marked broken can produce another transaction-management error instead of the intended observation.

Complete worked example

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.db import models, transaction, IntegrityError

class Role(models.Model):
    title = models.CharField(max_length=80)
    country = models.CharField(max_length=2)
    class Meta:
        app_label = "framework_lab"
        constraints = [models.CheckConstraint(condition=models.Q(country__in=["US", "IL", "UA"]), name="role_country_allowed")]

install_models(Role)

def save_country(country):
    Role.objects.all().delete()
    try:
        with transaction.atomic():
            Role.objects.create(title="Engineer", country=country)
    except IntegrityError:
        return {"accepted": False, "count": Role.objects.count()}
    return {"accepted": True, "count": Role.objects.count()}
Reasoning
The model has ordinary text fields plus a named constraint requiring an allowed country. The helper clears only its fictional fixture rows, attempts one insert inside atomic and converts an IntegrityError into an explicit rejected result.

Counterexamples and common mistakes

The counterexample removes the database constraint.

The counterexample removes the database constraint. An unsupported country can then be saved despite the declared invariant.

Do not assume max_length or a choices list proves identical enforcement on every database backend; inspect the actual generated schema and validation path. Also avoid swallowing every database exception as an invalid country. A connection failure or an unrelated constraint violation needs a different diagnosis in a real service.

Counterexample: a runnable framework mistake

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.db import models, transaction, IntegrityError

class Role(models.Model):
    title = models.CharField(max_length=80)
    country = models.CharField(max_length=2)
    class Meta:
        app_label = "framework_lab"
        constraints = []

install_models(Role)

def save_country(country):
    Role.objects.all().delete()
    try:
        with transaction.atomic():
            Role.objects.create(title="Engineer", country=country)
    except IntegrityError:
        return {"accepted": False, "count": Role.objects.count()}
    return {"accepted": True, "count": Role.objects.count()}
Reasoning
The counterexample removes the database constraint. An unsupported country can then be saved despite the declared invariant.

Independent practice: one bounded framework change

Add Canada to the database-enforced set while preserving rejection of an unknown country.

Add Canada to the database-enforced set while preserving rejection of an unknown country. Change the model constraint used by this disposable fixture, then run the accepted and rejected cases again.

In an existing deployed application, that model change would require a reviewed migration. The next migration lesson demonstrates state changes rather than suggesting that editing Python metadata alone updates an already existing database.

Step 1: Allow Canada in the database constraint. Step 2: Keep an unsupported country rejected with no saved row.

Practice starter: solve before reading the solution

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.db import models, transaction, IntegrityError

class Role(models.Model):
    title = models.CharField(max_length=80)
    country = models.CharField(max_length=2)
    class Meta:
        app_label = "framework_lab"
        constraints = [models.CheckConstraint(condition=models.Q(country__in=["US", "IL", "UA"]), name="role_country_allowed")]

install_models(Role)

def save_country(country):
    # Remove this placeholder, then adapt the retained baseline:
    # Allow Canada in the database constraint.
    raise NotImplementedError
    Role.objects.all().delete()
    try:
        with transaction.atomic():
            Role.objects.create(title="Engineer", country=country)
    except IntegrityError:
        return {"accepted": False, "count": Role.objects.count()}
    return {"accepted": True, "count": Role.objects.count()}
Reasoning
Allow Canada in the database constraint. Keep an unsupported country rejected with no saved row.
  • Hint 1: Update the explicit allowed-code set, not the exception handler.
  • Hint 2: Inspect count only after the failed atomic block has exited.

Solution, tests and trade-offs

The acceptance cases compare both the outcome and the actual remaining row count.

The acceptance cases compare both the outcome and the actual remaining row count. A rejected insert must leave no saved row in this fixture, while accepted values produce exactly one.

Practice adds the new allowed code without turning the constraint into an unrestricted string field. These checks use SQLite through Django and do not establish identical SQL behavior, locking or operational migration costs on every production database engine.

Practice solution and acceptance checks

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.db import models, transaction, IntegrityError

class Role(models.Model):
    title = models.CharField(max_length=80)
    country = models.CharField(max_length=2)
    class Meta:
        app_label = "framework_lab"
        constraints = [models.CheckConstraint(condition=models.Q(country__in=["US", "IL", "UA", "CA"]), name="role_country_allowed")]

install_models(Role)

def save_country(country):
    Role.objects.all().delete()
    try:
        with transaction.atomic():
            Role.objects.create(title="Engineer", country=country)
    except IntegrityError:
        return {"accepted": False, "count": Role.objects.count()}
    return {"accepted": True, "count": Role.objects.count()}
Reasoning
The acceptance cases compare both the outcome and the actual remaining row count. A rejected insert must leave no saved row in this fixture, while accepted values produce exactly one.

Interview reasoning and next step

Explain which invariants should survive every write path, including scripts and concurrent requests.

Explain which invariants should survive every write path, including scripts and concurrent requests. Form validation improves user feedback, while database constraints protect persisted state when a write reaches the database.

Keep a useful constraint name for diagnostics and review generated migrations before deployment. The next lessons use querysets and relationships without weakening these distinctions between accepted input, stored identity and the selected reporting population.

Optional stretch — after the core task works: Generate and inspect the corresponding migration in a separate disposable project.

Computer practice: https://applydjinn.com/typorithm/python-frameworks-lab-20261105.zip. Install the pinned dependencies in an isolated environment, run verify.py --track django, then edit the retained exercise baseline and check it with --practice dj-models. The lab uses owned in-memory databases, local framework clients and tiny CPU tensors. It never reads the working ApplyDjinn database, contacts production feeds or downloads models.

Distinguish model field metadata from a constraint that the database actually enforces.