Plain is headed towards 1.0! Subscribe for development updates →

plain.models

Model your data and store it in a database.

# app/users/models.py
from plain import models
from plain.passwords.models import PasswordField


class User(models.Model):
    email = models.EmailField()
    password = PasswordField()
    is_admin = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.email

Create, update, and delete instances of your models:

from .models import User


# Create a new user
user = User.objects.create(
    email="[email protected]",
    password="password",
)

# Update a user
user.email = "[email protected]"
user.save()

# Delete a user
user.delete()

# Query for users
admin_users = User.objects.filter(is_admin=True)

Installation

# app/settings.py
INSTALLED_PACKAGES = [
    ...
    "plain.models",
]

To connect to a database, you can provide a DATABASE_URL environment variable.

DATABASE_URL=postgresql://user:password@localhost:5432/dbname

Or you can manually define the DATABASES setting.

# app/settings.py
DATABASES = {
    "default": {
        "ENGINE": "plain.models.backends.postgresql",
        "NAME": "dbname",
        "USER": "user",
        "PASSWORD": "password",
        "HOST": "localhost",
        "PORT": "5432",
    }
}

Multiple backends are supported, including Postgres, MySQL, and SQLite.

Querying

Migrations

Migration docs

Fields

Field docs

Validation

Indexes and constraints

Managers

Forms

   1import os
   2import subprocess
   3import sys
   4import time
   5from itertools import takewhile
   6
   7import click
   8
   9from plain.cli import register_cli
  10from plain.packages import packages_registry
  11from plain.runtime import settings
  12from plain.utils.text import Truncator
  13
  14from . import migrations
  15from .backups.cli import cli as backups_cli
  16from .backups.cli import create_backup
  17from .db import DEFAULT_DB_ALIAS, OperationalError, connections, router
  18from .migrations.autodetector import MigrationAutodetector
  19from .migrations.executor import MigrationExecutor
  20from .migrations.loader import AmbiguityError, MigrationLoader
  21from .migrations.migration import Migration, SettingsTuple
  22from .migrations.optimizer import MigrationOptimizer
  23from .migrations.questioner import (
  24    InteractiveMigrationQuestioner,
  25    MigrationQuestioner,
  26    NonInteractiveMigrationQuestioner,
  27)
  28from .migrations.recorder import MigrationRecorder
  29from .migrations.state import ModelState, ProjectState
  30from .migrations.utils import get_migration_name_timestamp
  31from .migrations.writer import MigrationWriter
  32from .registry import models_registry
  33
  34
  35@register_cli("models")
  36@click.group()
  37def cli():
  38    pass
  39
  40
  41cli.add_command(backups_cli)
  42
  43
  44@cli.command()
  45@click.option(
  46    "--database",
  47    default=DEFAULT_DB_ALIAS,
  48    help=(
  49        "Nominates a database onto which to open a shell. Defaults to the "
  50        '"default" database.'
  51    ),
  52)
  53@click.argument("parameters", nargs=-1)
  54def db_shell(database, parameters):
  55    """Runs the command-line client for specified database, or the default database if none is provided."""
  56    connection = connections[database]
  57    try:
  58        connection.client.runshell(parameters)
  59    except FileNotFoundError:
  60        # Note that we're assuming the FileNotFoundError relates to the
  61        # command missing. It could be raised for some other reason, in
  62        # which case this error message would be inaccurate. Still, this
  63        # message catches the common case.
  64        click.secho(
  65            f"You appear not to have the {connection.client.executable_name!r} program installed or on your path.",
  66            fg="red",
  67            err=True,
  68        )
  69        sys.exit(1)
  70    except subprocess.CalledProcessError as e:
  71        click.secho(
  72            '"{}" returned non-zero exit status {}.'.format(
  73                " ".join(e.cmd),
  74                e.returncode,
  75            ),
  76            fg="red",
  77            err=True,
  78        )
  79        sys.exit(e.returncode)
  80
  81
  82@cli.command()
  83def db_wait():
  84    """Wait for the database to be ready"""
  85    attempts = 0
  86    while True:
  87        attempts += 1
  88        waiting_for = []
  89
  90        for conn in connections.all():
  91            try:
  92                conn.ensure_connection()
  93            except OperationalError:
  94                waiting_for.append(conn.alias)
  95
  96        if waiting_for:
  97            click.secho(
  98                f"Waiting for database (attempt {attempts}): {', '.join(waiting_for)}",
  99                fg="yellow",
 100            )
 101            time.sleep(1.5)
 102        else:
 103            click.secho(f"Database ready: {', '.join(connections)}", fg="green")
 104            break
 105
 106
 107@register_cli("makemigrations")
 108@cli.command()
 109@click.argument("package_labels", nargs=-1)
 110@click.option(
 111    "--dry-run",
 112    is_flag=True,
 113    help="Just show what migrations would be made; don't actually write them.",
 114)
 115@click.option("--merge", is_flag=True, help="Enable fixing of migration conflicts.")
 116@click.option("--empty", is_flag=True, help="Create an empty migration.")
 117@click.option(
 118    "--noinput",
 119    "--no-input",
 120    "no_input",
 121    is_flag=True,
 122    help="Tells Plain to NOT prompt the user for input of any kind.",
 123)
 124@click.option("-n", "--name", help="Use this name for migration file(s).")
 125@click.option(
 126    "--check",
 127    is_flag=True,
 128    help="Exit with a non-zero status if model changes are missing migrations and don't actually write them.",
 129)
 130@click.option(
 131    "-v",
 132    "--verbosity",
 133    type=int,
 134    default=1,
 135    help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
 136)
 137def makemigrations(
 138    package_labels, dry_run, merge, empty, no_input, name, check, verbosity
 139):
 140    """Creates new migration(s) for packages."""
 141
 142    written_files = []
 143    interactive = not no_input
 144    migration_name = name
 145    check_changes = check
 146
 147    def log(msg, level=1):
 148        if verbosity >= level:
 149            click.echo(msg)
 150
 151    def write_migration_files(changes, update_previous_migration_paths=None):
 152        """Take a changes dict and write them out as migration files."""
 153        directory_created = {}
 154        for package_label, package_migrations in changes.items():
 155            log(
 156                click.style(f"Migrations for '{package_label}':", fg="cyan", bold=True),
 157                level=1,
 158            )
 159            for migration in package_migrations:
 160                writer = MigrationWriter(migration)
 161                migration_string = os.path.relpath(writer.path)
 162                log(f"  {click.style(migration_string, fg='yellow')}\n", level=1)
 163                for operation in migration.operations:
 164                    log(f"    - {operation.describe()}", level=1)
 165
 166                if not dry_run:
 167                    migrations_directory = os.path.dirname(writer.path)
 168                    if not directory_created.get(package_label):
 169                        os.makedirs(migrations_directory, exist_ok=True)
 170                        init_path = os.path.join(migrations_directory, "__init__.py")
 171                        if not os.path.isfile(init_path):
 172                            open(init_path, "w").close()
 173                        directory_created[package_label] = True
 174
 175                    migration_string = writer.as_string()
 176                    with open(writer.path, "w", encoding="utf-8") as fh:
 177                        fh.write(migration_string)
 178                        written_files.append(writer.path)
 179
 180                    if update_previous_migration_paths:
 181                        prev_path = update_previous_migration_paths[package_label]
 182                        if writer.needs_manual_porting:
 183                            log(
 184                                click.style(
 185                                    f"Updated migration {migration_string} requires manual porting.\n"
 186                                    f"Previous migration {os.path.relpath(prev_path)} was kept and "
 187                                    f"must be deleted after porting functions manually.",
 188                                    fg="yellow",
 189                                ),
 190                                level=1,
 191                            )
 192                        else:
 193                            os.remove(prev_path)
 194                            log(f"Deleted {os.path.relpath(prev_path)}", level=1)
 195                elif verbosity >= 3:
 196                    log(
 197                        click.style(
 198                            f"Full migrations file '{writer.filename}':",
 199                            fg="cyan",
 200                            bold=True,
 201                        ),
 202                        level=3,
 203                    )
 204                    log(writer.as_string(), level=3)
 205
 206    def handle_merge(loader, conflicts):
 207        """Handle merging conflicting migrations."""
 208        if interactive:
 209            questioner = InteractiveMigrationQuestioner()
 210        else:
 211            questioner = MigrationQuestioner(defaults={"ask_merge": True})
 212
 213        for package_label, migration_names in conflicts.items():
 214            log(click.style(f"Merging {package_label}", fg="cyan", bold=True), level=1)
 215
 216            merge_migrations = []
 217            for migration_name in migration_names:
 218                migration = loader.get_migration(package_label, migration_name)
 219                migration.ancestry = [
 220                    mig
 221                    for mig in loader.graph.forwards_plan(
 222                        (package_label, migration_name)
 223                    )
 224                    if mig[0] == migration.package_label
 225                ]
 226                merge_migrations.append(migration)
 227
 228            def all_items_equal(seq):
 229                return all(item == seq[0] for item in seq[1:])
 230
 231            merge_migrations_generations = zip(*(m.ancestry for m in merge_migrations))
 232            common_ancestor_count = sum(
 233                1 for _ in takewhile(all_items_equal, merge_migrations_generations)
 234            )
 235            if not common_ancestor_count:
 236                raise ValueError(f"Could not find common ancestor of {migration_names}")
 237
 238            for migration in merge_migrations:
 239                migration.branch = migration.ancestry[common_ancestor_count:]
 240                migrations_ops = (
 241                    loader.get_migration(node_package, node_name).operations
 242                    for node_package, node_name in migration.branch
 243                )
 244                migration.merged_operations = sum(migrations_ops, [])
 245
 246            for migration in merge_migrations:
 247                log(click.style(f"  Branch {migration.name}", fg="yellow"), level=1)
 248                for operation in migration.merged_operations:
 249                    log(f"    - {operation.describe()}", level=1)
 250
 251            if questioner.ask_merge(package_label):
 252                numbers = [
 253                    MigrationAutodetector.parse_number(migration.name)
 254                    for migration in merge_migrations
 255                ]
 256                biggest_number = (
 257                    max(x for x in numbers if x is not None) if numbers else 0
 258                )
 259
 260                subclass = type(
 261                    "Migration",
 262                    (Migration,),
 263                    {
 264                        "dependencies": [
 265                            (package_label, migration.name)
 266                            for migration in merge_migrations
 267                        ],
 268                    },
 269                )
 270
 271                parts = [f"{biggest_number + 1:04d}"]
 272                if migration_name:
 273                    parts.append(migration_name)
 274                else:
 275                    parts.append("merge")
 276                    leaf_names = "_".join(
 277                        sorted(migration.name for migration in merge_migrations)
 278                    )
 279                    if len(leaf_names) > 47:
 280                        parts.append(get_migration_name_timestamp())
 281                    else:
 282                        parts.append(leaf_names)
 283
 284                new_migration_name = "_".join(parts)
 285                new_migration = subclass(new_migration_name, package_label)
 286                writer = MigrationWriter(new_migration)
 287
 288                if not dry_run:
 289                    with open(writer.path, "w", encoding="utf-8") as fh:
 290                        fh.write(writer.as_string())
 291                    log(f"\nCreated new merge migration {writer.path}", level=1)
 292                elif verbosity == 3:
 293                    log(
 294                        click.style(
 295                            f"Full merge migrations file '{writer.filename}':",
 296                            fg="cyan",
 297                            bold=True,
 298                        ),
 299                        level=3,
 300                    )
 301                    log(writer.as_string(), level=3)
 302
 303    # Validate package labels
 304    package_labels = set(package_labels)
 305    has_bad_labels = False
 306    for package_label in package_labels:
 307        try:
 308            packages_registry.get_package_config(package_label)
 309        except LookupError as err:
 310            click.echo(str(err), err=True)
 311            has_bad_labels = True
 312    if has_bad_labels:
 313        sys.exit(2)
 314
 315    # Load the current graph state
 316    loader = MigrationLoader(None, ignore_no_migrations=True)
 317
 318    # Raise an error if any migrations are applied before their dependencies.
 319    consistency_check_labels = {
 320        config.label for config in packages_registry.get_package_configs()
 321    }
 322    # Non-default databases are only checked if database routers used.
 323    aliases_to_check = connections if settings.DATABASE_ROUTERS else [DEFAULT_DB_ALIAS]
 324    for alias in sorted(aliases_to_check):
 325        connection = connections[alias]
 326        if connection.settings_dict["ENGINE"] != "plain.models.backends.dummy" and any(
 327            router.allow_migrate(
 328                connection.alias, package_label, model_name=model._meta.object_name
 329            )
 330            for package_label in consistency_check_labels
 331            for model in models_registry.get_models(package_label=package_label)
 332        ):
 333            loader.check_consistent_history(connection)
 334
 335    # Check for conflicts
 336    conflicts = loader.detect_conflicts()
 337    if package_labels:
 338        conflicts = {
 339            package_label: conflict
 340            for package_label, conflict in conflicts.items()
 341            if package_label in package_labels
 342        }
 343
 344    if conflicts and not merge:
 345        name_str = "; ".join(
 346            "{} in {}".format(", ".join(names), package)
 347            for package, names in conflicts.items()
 348        )
 349        raise click.ClickException(
 350            f"Conflicting migrations detected; multiple leaf nodes in the "
 351            f"migration graph: ({name_str}).\nTo fix them run "
 352            f"'python manage.py makemigrations --merge'"
 353        )
 354
 355    # Handle merge if requested
 356    if merge and conflicts:
 357        return handle_merge(loader, conflicts)
 358
 359    # Set up questioner
 360    if interactive:
 361        questioner = InteractiveMigrationQuestioner(
 362            specified_packages=package_labels,
 363            dry_run=dry_run,
 364        )
 365    else:
 366        questioner = NonInteractiveMigrationQuestioner(
 367            specified_packages=package_labels,
 368            dry_run=dry_run,
 369            verbosity=verbosity,
 370        )
 371
 372    # Set up autodetector
 373    autodetector = MigrationAutodetector(
 374        loader.project_state(),
 375        ProjectState.from_models_registry(models_registry),
 376        questioner,
 377    )
 378
 379    # Handle empty migrations if requested
 380    if empty:
 381        if not package_labels:
 382            raise click.ClickException(
 383                "You must supply at least one package label when using --empty."
 384            )
 385        changes = {
 386            package: [Migration("custom", package)] for package in package_labels
 387        }
 388        changes = autodetector.arrange_for_graph(
 389            changes=changes,
 390            graph=loader.graph,
 391            migration_name=migration_name,
 392        )
 393        write_migration_files(changes)
 394        return
 395
 396    # Detect changes
 397    changes = autodetector.changes(
 398        graph=loader.graph,
 399        trim_to_packages=package_labels or None,
 400        convert_packages=package_labels or None,
 401        migration_name=migration_name,
 402    )
 403
 404    if not changes:
 405        log(
 406            "No changes detected"
 407            if not package_labels
 408            else f"No changes detected in {'package' if len(package_labels) == 1 else 'packages'} "
 409            f"'{', '.join(package_labels)}'",
 410            level=1,
 411        )
 412    else:
 413        if check_changes:
 414            sys.exit(1)
 415
 416        write_migration_files(changes)
 417
 418
 419@register_cli("migrate")
 420@cli.command()
 421@click.argument("package_label", required=False)
 422@click.argument("migration_name", required=False)
 423@click.option(
 424    "--database",
 425    default=DEFAULT_DB_ALIAS,
 426    help="Nominates a database to synchronize. Defaults to the 'default' database.",
 427)
 428@click.option(
 429    "--fake", is_flag=True, help="Mark migrations as run without actually running them."
 430)
 431@click.option(
 432    "--fake-initial",
 433    is_flag=True,
 434    help="Detect if tables already exist and fake-apply initial migrations if so. Make sure that the current database schema matches your initial migration before using this flag. Plain will only check for an existing table name.",
 435)
 436@click.option(
 437    "--plan",
 438    is_flag=True,
 439    help="Shows a list of the migration actions that will be performed.",
 440)
 441@click.option(
 442    "--check",
 443    "check_unapplied",
 444    is_flag=True,
 445    help="Exits with a non-zero status if unapplied migrations exist and does not actually apply migrations.",
 446)
 447@click.option(
 448    "--backup/--no-backup",
 449    "backup",
 450    is_flag=True,
 451    default=None,
 452    help="Explicitly enable/disable pre-migration backups.",
 453)
 454@click.option(
 455    "--prune",
 456    is_flag=True,
 457    help="Delete nonexistent migrations from the plainmigrations table.",
 458)
 459@click.option(
 460    "-v",
 461    "--verbosity",
 462    type=int,
 463    default=1,
 464    help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
 465)
 466def migrate(
 467    package_label,
 468    migration_name,
 469    database,
 470    fake,
 471    fake_initial,
 472    plan,
 473    check_unapplied,
 474    backup,
 475    prune,
 476    verbosity,
 477):
 478    """Updates database schema. Manages both packages with migrations and those without."""
 479
 480    def migration_progress_callback(action, migration=None, fake=False):
 481        if verbosity >= 1:
 482            compute_time = verbosity > 1
 483            if action == "apply_start":
 484                if compute_time:
 485                    start = time.monotonic()
 486                click.echo(f"  Applying {migration}...", nl=False)
 487            elif action == "apply_success":
 488                elapsed = f" ({time.monotonic() - start:.3f}s)" if compute_time else ""
 489                if fake:
 490                    click.echo(click.style(f" FAKED{elapsed}", fg="green"))
 491                else:
 492                    click.echo(click.style(f" OK{elapsed}", fg="green"))
 493            elif action == "render_start":
 494                if compute_time:
 495                    start = time.monotonic()
 496                click.echo("  Rendering model states...", nl=False)
 497            elif action == "render_success":
 498                elapsed = f" ({time.monotonic() - start:.3f}s)" if compute_time else ""
 499                click.echo(click.style(f" DONE{elapsed}", fg="green"))
 500
 501    def describe_operation(operation):
 502        """Return a string that describes a migration operation for --plan."""
 503        prefix = ""
 504        is_error = False
 505        if hasattr(operation, "code"):
 506            code = operation.code
 507            action = (code.__doc__ or "") if code else None
 508        elif hasattr(operation, "sql"):
 509            action = operation.sql
 510        else:
 511            action = ""
 512        if action is not None:
 513            action = str(action).replace("\n", "")
 514        if action:
 515            action = " -> " + action
 516        truncated = Truncator(action)
 517        return prefix + operation.describe() + truncated.chars(40), is_error
 518
 519    # Get the database we're operating from
 520    connection = connections[database]
 521
 522    # Hook for backends needing any database preparation
 523    connection.prepare_database()
 524
 525    # Work out which packages have migrations and which do not
 526    executor = MigrationExecutor(connection, migration_progress_callback)
 527
 528    # Raise an error if any migrations are applied before their dependencies.
 529    executor.loader.check_consistent_history(connection)
 530
 531    # Before anything else, see if there's conflicting packages and drop out
 532    # hard if there are any
 533    conflicts = executor.loader.detect_conflicts()
 534    if conflicts:
 535        name_str = "; ".join(
 536            "{} in {}".format(", ".join(names), package)
 537            for package, names in conflicts.items()
 538        )
 539        raise click.ClickException(
 540            "Conflicting migrations detected; multiple leaf nodes in the "
 541            f"migration graph: ({name_str}).\nTo fix them run "
 542            "'python manage.py makemigrations --merge'"
 543        )
 544
 545    # If they supplied command line arguments, work out what they mean.
 546    target_package_labels_only = True
 547    if package_label:
 548        try:
 549            packages_registry.get_package_config(package_label)
 550        except LookupError as err:
 551            raise click.ClickException(str(err))
 552
 553        if package_label not in executor.loader.migrated_packages:
 554            raise click.ClickException(
 555                f"Package '{package_label}' does not have migrations."
 556            )
 557
 558    if package_label and migration_name:
 559        try:
 560            migration = executor.loader.get_migration_by_prefix(
 561                package_label, migration_name
 562            )
 563        except AmbiguityError:
 564            raise click.ClickException(
 565                f"More than one migration matches '{migration_name}' in package '{package_label}'. "
 566                "Please be more specific."
 567            )
 568        except KeyError:
 569            raise click.ClickException(
 570                f"Cannot find a migration matching '{migration_name}' from package '{package_label}'."
 571            )
 572        target = (package_label, migration.name)
 573        if (
 574            target not in executor.loader.graph.nodes
 575            and target in executor.loader.replacements
 576        ):
 577            incomplete_migration = executor.loader.replacements[target]
 578            target = incomplete_migration.replaces[-1]
 579        targets = [target]
 580        target_package_labels_only = False
 581    elif package_label:
 582        targets = [
 583            key for key in executor.loader.graph.leaf_nodes() if key[0] == package_label
 584        ]
 585    else:
 586        targets = executor.loader.graph.leaf_nodes()
 587
 588    if prune:
 589        if not package_label:
 590            raise click.ClickException(
 591                "Migrations can be pruned only when a package is specified."
 592            )
 593        if verbosity > 0:
 594            click.echo("Pruning migrations:", color="cyan")
 595        to_prune = set(executor.loader.applied_migrations) - set(
 596            executor.loader.disk_migrations
 597        )
 598        squashed_migrations_with_deleted_replaced_migrations = [
 599            migration_key
 600            for migration_key, migration_obj in executor.loader.replacements.items()
 601            if any(replaced in to_prune for replaced in migration_obj.replaces)
 602        ]
 603        if squashed_migrations_with_deleted_replaced_migrations:
 604            click.echo(
 605                click.style(
 606                    "  Cannot use --prune because the following squashed "
 607                    "migrations have their 'replaces' attributes and may not "
 608                    "be recorded as applied:",
 609                    fg="yellow",
 610                )
 611            )
 612            for migration in squashed_migrations_with_deleted_replaced_migrations:
 613                package, name = migration
 614                click.echo(f"    {package}.{name}")
 615            click.echo(
 616                click.style(
 617                    "  Re-run 'manage.py migrate' if they are not marked as "
 618                    "applied, and remove 'replaces' attributes in their "
 619                    "Migration classes.",
 620                    fg="yellow",
 621                )
 622            )
 623        else:
 624            to_prune = sorted(
 625                migration for migration in to_prune if migration[0] == package_label
 626            )
 627            if to_prune:
 628                for migration in to_prune:
 629                    package, name = migration
 630                    if verbosity > 0:
 631                        click.echo(
 632                            click.style(f"  Pruning {package}.{name}", fg="yellow"),
 633                            nl=False,
 634                        )
 635                    executor.recorder.record_unapplied(package, name)
 636                    if verbosity > 0:
 637                        click.echo(click.style(" OK", fg="green"))
 638            elif verbosity > 0:
 639                click.echo("  No migrations to prune.")
 640
 641    migration_plan = executor.migration_plan(targets)
 642
 643    if plan:
 644        click.echo("Planned operations:", color="cyan")
 645        if not migration_plan:
 646            click.echo("  No planned migration operations.")
 647        else:
 648            for migration in migration_plan:
 649                click.echo(str(migration), color="cyan")
 650                for operation in migration.operations:
 651                    message, is_error = describe_operation(operation)
 652                    if is_error:
 653                        click.echo("    " + message, fg="yellow")
 654                    else:
 655                        click.echo("    " + message)
 656        if check_unapplied:
 657            sys.exit(1)
 658        return
 659
 660    if check_unapplied:
 661        if migration_plan:
 662            sys.exit(1)
 663        return
 664
 665    if prune:
 666        return
 667
 668    # Print some useful info
 669    if verbosity >= 1:
 670        click.echo("Operations to perform:", color="cyan")
 671
 672        if target_package_labels_only:
 673            click.echo(
 674                "  Apply all migrations: "
 675                + (", ".join(sorted({a for a, n in targets})) or "(none)"),
 676                color="yellow",
 677            )
 678        else:
 679            click.echo(
 680                f"  Target specific migration: {targets[0][1]}, from {targets[0][0]}",
 681                color="yellow",
 682            )
 683
 684    pre_migrate_state = executor._create_project_state(with_applied_migrations=True)
 685
 686    # sql = executor.loader.collect_sql(migration_plan)
 687    # pprint(sql)
 688
 689    if migration_plan:
 690        if backup or (
 691            backup is None
 692            and settings.DEBUG
 693            and click.confirm(
 694                "\nYou are in DEBUG mode. Would you like to make a database backup before running migrations?",
 695                default=True,
 696            )
 697        ):
 698            backup_name = f"migrate_{time.strftime('%Y%m%d_%H%M%S')}"
 699            # Can't use ctx.invoke because this is called by the test db creation currently,
 700            # which doesn't have a context.
 701            create_backup.callback(
 702                backup_name=backup_name,
 703                pg_dump=os.environ.get(
 704                    "PG_DUMP", "pg_dump"
 705                ),  # Have to this again manually
 706            )
 707            print()
 708
 709        if verbosity >= 1:
 710            click.echo("Running migrations:", color="cyan")
 711
 712        post_migrate_state = executor.migrate(
 713            targets,
 714            plan=migration_plan,
 715            state=pre_migrate_state.clone(),
 716            fake=fake,
 717            fake_initial=fake_initial,
 718        )
 719        # post_migrate signals have access to all models. Ensure that all models
 720        # are reloaded in case any are delayed.
 721        post_migrate_state.clear_delayed_models_cache()
 722        post_migrate_packages = post_migrate_state.models_registry
 723
 724        # Re-render models of real packages to include relationships now that
 725        # we've got a final state. This wouldn't be necessary if real packages
 726        # models were rendered with relationships in the first place.
 727        with post_migrate_packages.bulk_update():
 728            model_keys = []
 729            for model_state in post_migrate_packages.real_models:
 730                model_key = model_state.package_label, model_state.name_lower
 731                model_keys.append(model_key)
 732                post_migrate_packages.unregister_model(*model_key)
 733        post_migrate_packages.render_multiple(
 734            [
 735                ModelState.from_model(models_registry.get_model(*model))
 736                for model in model_keys
 737            ]
 738        )
 739
 740    elif verbosity >= 1:
 741        click.echo("  No migrations to apply.")
 742        # If there's changes that aren't in migrations yet, tell them
 743        # how to fix it.
 744        autodetector = MigrationAutodetector(
 745            executor.loader.project_state(),
 746            ProjectState.from_models_registry(models_registry),
 747        )
 748        changes = autodetector.changes(graph=executor.loader.graph)
 749        if changes:
 750            click.echo(
 751                click.style(
 752                    f"  Your models in package(s): {', '.join(repr(package) for package in sorted(changes))} "
 753                    "have changes that are not yet reflected in a migration, and so won't be applied.",
 754                    fg="yellow",
 755                )
 756            )
 757            click.echo(
 758                click.style(
 759                    "  Run 'manage.py makemigrations' to make new "
 760                    "migrations, and then re-run 'manage.py migrate' to "
 761                    "apply them.",
 762                    fg="yellow",
 763                )
 764            )
 765
 766
 767@cli.command()
 768@click.argument("package_label")
 769@click.argument("migration_name")
 770@click.option(
 771    "--check",
 772    is_flag=True,
 773    help="Exit with a non-zero status if the migration can be optimized.",
 774)
 775@click.option(
 776    "-v",
 777    "--verbosity",
 778    type=int,
 779    default=1,
 780    help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
 781)
 782def optimize_migration(package_label, migration_name, check, verbosity):
 783    """Optimizes the operations for the named migration."""
 784    try:
 785        packages_registry.get_package_config(package_label)
 786    except LookupError as err:
 787        raise click.ClickException(str(err))
 788
 789    # Load the current graph state.
 790    loader = MigrationLoader(None)
 791    if package_label not in loader.migrated_packages:
 792        raise click.ClickException(
 793            f"Package '{package_label}' does not have migrations."
 794        )
 795
 796    # Find a migration.
 797    try:
 798        migration = loader.get_migration_by_prefix(package_label, migration_name)
 799    except AmbiguityError:
 800        raise click.ClickException(
 801            f"More than one migration matches '{migration_name}' in package "
 802            f"'{package_label}'. Please be more specific."
 803        )
 804    except KeyError:
 805        raise click.ClickException(
 806            f"Cannot find a migration matching '{migration_name}' from package "
 807            f"'{package_label}'."
 808        )
 809
 810    # Optimize the migration.
 811    optimizer = MigrationOptimizer()
 812    new_operations = optimizer.optimize(migration.operations, migration.package_label)
 813    if len(migration.operations) == len(new_operations):
 814        if verbosity > 0:
 815            click.echo("No optimizations possible.")
 816        return
 817    else:
 818        if verbosity > 0:
 819            click.echo(
 820                f"Optimizing from {len(migration.operations)} operations to {len(new_operations)} operations."
 821            )
 822        if check:
 823            sys.exit(1)
 824
 825    # Set the new migration optimizations.
 826    migration.operations = new_operations
 827
 828    # Write out the optimized migration file.
 829    writer = MigrationWriter(migration)
 830    migration_file_string = writer.as_string()
 831    if writer.needs_manual_porting:
 832        if migration.replaces:
 833            raise click.ClickException(
 834                "Migration will require manual porting but is already a squashed "
 835                "migration.\nTransition to a normal migration first."
 836            )
 837        # Make a new migration with those operations.
 838        subclass = type(
 839            "Migration",
 840            (migrations.Migration,),
 841            {
 842                "dependencies": migration.dependencies,
 843                "operations": new_operations,
 844                "replaces": [(migration.package_label, migration.name)],
 845            },
 846        )
 847        optimized_migration_name = f"{migration.name}_optimized"
 848        optimized_migration = subclass(optimized_migration_name, package_label)
 849        writer = MigrationWriter(optimized_migration)
 850        migration_file_string = writer.as_string()
 851        if verbosity > 0:
 852            click.echo(click.style("Manual porting required", fg="yellow", bold=True))
 853            click.echo(
 854                "  Your migrations contained functions that must be manually "
 855                "copied over,\n"
 856                "  as we could not safely copy their implementation.\n"
 857                "  See the comment at the top of the optimized migration for "
 858                "details."
 859            )
 860
 861    with open(writer.path, "w", encoding="utf-8") as fh:
 862        fh.write(migration_file_string)
 863
 864    if verbosity > 0:
 865        click.echo(
 866            click.style(f"Optimized migration {writer.path}", fg="green", bold=True)
 867        )
 868
 869
 870@cli.command()
 871@click.argument("package_labels", nargs=-1)
 872@click.option(
 873    "--database",
 874    default=DEFAULT_DB_ALIAS,
 875    help="Nominates a database to show migrations for. Defaults to the 'default' database.",
 876)
 877@click.option(
 878    "--format",
 879    type=click.Choice(["list", "plan"]),
 880    default="list",
 881    help="Output format.",
 882)
 883@click.option(
 884    "-v",
 885    "--verbosity",
 886    type=int,
 887    default=1,
 888    help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
 889)
 890def show_migrations(package_labels, database, format, verbosity):
 891    """Shows all available migrations for the current project"""
 892
 893    def _validate_package_names(package_names):
 894        has_bad_names = False
 895        for package_name in package_names:
 896            try:
 897                packages_registry.get_package_config(package_name)
 898            except LookupError as err:
 899                click.echo(str(err), err=True)
 900                has_bad_names = True
 901        if has_bad_names:
 902            sys.exit(2)
 903
 904    def show_list(connection, package_names):
 905        """
 906        Show a list of all migrations on the system, or only those of
 907        some named packages.
 908        """
 909        # Load migrations from disk/DB
 910        loader = MigrationLoader(connection, ignore_no_migrations=True)
 911        recorder = MigrationRecorder(connection)
 912        recorded_migrations = recorder.applied_migrations()
 913
 914        graph = loader.graph
 915        # If we were passed a list of packages, validate it
 916        if package_names:
 917            _validate_package_names(package_names)
 918        # Otherwise, show all packages in alphabetic order
 919        else:
 920            package_names = sorted(loader.migrated_packages)
 921        # For each app, print its migrations in order from oldest (roots) to
 922        # newest (leaves).
 923        for package_name in package_names:
 924            click.secho(package_name, fg="cyan", bold=True)
 925            shown = set()
 926            for node in graph.leaf_nodes(package_name):
 927                for plan_node in graph.forwards_plan(node):
 928                    if plan_node not in shown and plan_node[0] == package_name:
 929                        # Give it a nice title if it's a squashed one
 930                        title = plan_node[1]
 931                        if graph.nodes[plan_node].replaces:
 932                            title += f" ({len(graph.nodes[plan_node].replaces)} squashed migrations)"
 933                        applied_migration = loader.applied_migrations.get(plan_node)
 934                        # Mark it as applied/unapplied
 935                        if applied_migration:
 936                            if plan_node in recorded_migrations:
 937                                output = f" [X] {title}"
 938                            else:
 939                                title += " Run 'manage.py migrate' to finish recording."
 940                                output = f" [-] {title}"
 941                            if verbosity >= 2 and hasattr(applied_migration, "applied"):
 942                                output += f" (applied at {applied_migration.applied.strftime('%Y-%m-%d %H:%M:%S')})"
 943                            click.echo(output)
 944                        else:
 945                            click.echo(f" [ ] {title}")
 946                        shown.add(plan_node)
 947            # If we didn't print anything, then a small message
 948            if not shown:
 949                click.secho(" (no migrations)", fg="red")
 950
 951        # Find recorded migrations that aren't in the graph (prunable)
 952        prunable_migrations = [
 953            migration
 954            for migration in recorded_migrations
 955            if (
 956                migration not in loader.disk_migrations
 957                and (not package_names or migration[0] in package_names)
 958            )
 959        ]
 960
 961        if prunable_migrations:
 962            click.echo()
 963            click.secho(
 964                "Recorded migrations not in migration files (candidates for pruning):",
 965                fg="yellow",
 966                bold=True,
 967            )
 968            prunable_by_package = {}
 969            for migration in prunable_migrations:
 970                package, name = migration
 971                if package not in prunable_by_package:
 972                    prunable_by_package[package] = []
 973                prunable_by_package[package].append(name)
 974
 975            for package in sorted(prunable_by_package.keys()):
 976                click.secho(f"  {package}:", fg="yellow")
 977                for name in sorted(prunable_by_package[package]):
 978                    click.echo(f"    - {name}")
 979
 980    def show_plan(connection, package_names):
 981        """
 982        Show all known migrations (or only those of the specified package_names)
 983        in the order they will be applied.
 984        """
 985        # Load migrations from disk/DB
 986        loader = MigrationLoader(connection)
 987        graph = loader.graph
 988        if package_names:
 989            _validate_package_names(package_names)
 990            targets = [key for key in graph.leaf_nodes() if key[0] in package_names]
 991        else:
 992            targets = graph.leaf_nodes()
 993        plan = []
 994        seen = set()
 995
 996        # Generate the plan
 997        for target in targets:
 998            for migration in graph.forwards_plan(target):
 999                if migration not in seen:
1000                    node = graph.node_map[migration]
1001                    plan.append(node)
1002                    seen.add(migration)
1003
1004        # Output
1005        def print_deps(node):
1006            out = []
1007            for parent in sorted(node.parents):
1008                out.append(f"{parent.key[0]}.{parent.key[1]}")
1009            if out:
1010                return f" ... ({', '.join(out)})"
1011            return ""
1012
1013        for node in plan:
1014            deps = ""
1015            if verbosity >= 2:
1016                deps = print_deps(node)
1017            if node.key in loader.applied_migrations:
1018                click.echo(f"[X]  {node.key[0]}.{node.key[1]}{deps}")
1019            else:
1020                click.echo(f"[ ]  {node.key[0]}.{node.key[1]}{deps}")
1021        if not plan:
1022            click.secho("(no migrations)", fg="red")
1023
1024    # Get the database we're operating from
1025    connection = connections[database]
1026
1027    if format == "plan":
1028        show_plan(connection, package_labels)
1029    else:
1030        show_list(connection, package_labels)
1031
1032
1033@cli.command()
1034@click.argument("package_label")
1035@click.argument("start_migration_name", required=False)
1036@click.argument("migration_name")
1037@click.option(
1038    "--no-optimize",
1039    is_flag=True,
1040    help="Do not try to optimize the squashed operations.",
1041)
1042@click.option(
1043    "--noinput",
1044    "--no-input",
1045    "no_input",
1046    is_flag=True,
1047    help="Tells Plain to NOT prompt the user for input of any kind.",
1048)
1049@click.option("--squashed-name", help="Sets the name of the new squashed migration.")
1050@click.option(
1051    "-v",
1052    "--verbosity",
1053    type=int,
1054    default=1,
1055    help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
1056)
1057def squash_migrations(
1058    package_label,
1059    start_migration_name,
1060    migration_name,
1061    no_optimize,
1062    no_input,
1063    squashed_name,
1064    verbosity,
1065):
1066    """
1067    Squashes an existing set of migrations (from first until specified) into a single new one.
1068    """
1069    interactive = not no_input
1070
1071    def find_migration(loader, package_label, name):
1072        try:
1073            return loader.get_migration_by_prefix(package_label, name)
1074        except AmbiguityError:
1075            raise click.ClickException(
1076                f"More than one migration matches '{name}' in package '{package_label}'. Please be more specific."
1077            )
1078        except KeyError:
1079            raise click.ClickException(
1080                f"Cannot find a migration matching '{name}' from package '{package_label}'."
1081            )
1082
1083    # Validate package_label
1084    try:
1085        packages_registry.get_package_config(package_label)
1086    except LookupError as err:
1087        raise click.ClickException(str(err))
1088
1089    # Load the current graph state, check the app and migration they asked for exists
1090    loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])
1091    if package_label not in loader.migrated_packages:
1092        raise click.ClickException(
1093            f"Package '{package_label}' does not have migrations (so squashmigrations on it makes no sense)"
1094        )
1095
1096    migration = find_migration(loader, package_label, migration_name)
1097
1098    # Work out the list of predecessor migrations
1099    migrations_to_squash = [
1100        loader.get_migration(al, mn)
1101        for al, mn in loader.graph.forwards_plan(
1102            (migration.package_label, migration.name)
1103        )
1104        if al == migration.package_label
1105    ]
1106
1107    if start_migration_name:
1108        start_migration = find_migration(loader, package_label, start_migration_name)
1109        start = loader.get_migration(
1110            start_migration.package_label, start_migration.name
1111        )
1112        try:
1113            start_index = migrations_to_squash.index(start)
1114            migrations_to_squash = migrations_to_squash[start_index:]
1115        except ValueError:
1116            raise click.ClickException(
1117                f"The migration '{start_migration}' cannot be found. Maybe it comes after "
1118                f"the migration '{migration}'?\n"
1119                f"Have a look at:\n"
1120                f"  python manage.py showmigrations {package_label}\n"
1121                f"to debug this issue."
1122            )
1123
1124    # Tell them what we're doing and optionally ask if we should proceed
1125    if verbosity > 0 or interactive:
1126        click.secho("Will squash the following migrations:", fg="cyan", bold=True)
1127        for migration in migrations_to_squash:
1128            click.echo(f" - {migration.name}")
1129
1130        if interactive:
1131            if not click.confirm("Do you wish to proceed?"):
1132                return
1133
1134    # Load the operations from all those migrations and concat together,
1135    # along with collecting external dependencies and detecting double-squashing
1136    operations = []
1137    dependencies = set()
1138    # We need to take all dependencies from the first migration in the list
1139    # as it may be 0002 depending on 0001
1140    first_migration = True
1141    for smigration in migrations_to_squash:
1142        if smigration.replaces:
1143            raise click.ClickException(
1144                "You cannot squash squashed migrations! Please transition it to a "
1145                "normal migration first"
1146            )
1147        operations.extend(smigration.operations)
1148        for dependency in smigration.dependencies:
1149            if isinstance(dependency, SettingsTuple):
1150                dependencies.add(dependency)
1151            elif dependency[0] != smigration.package_label or first_migration:
1152                dependencies.add(dependency)
1153        first_migration = False
1154
1155    if no_optimize:
1156        if verbosity > 0:
1157            click.secho("(Skipping optimization.)", fg="yellow")
1158        new_operations = operations
1159    else:
1160        if verbosity > 0:
1161            click.secho("Optimizing...", fg="cyan")
1162
1163        optimizer = MigrationOptimizer()
1164        new_operations = optimizer.optimize(operations, migration.package_label)
1165
1166        if verbosity > 0:
1167            if len(new_operations) == len(operations):
1168                click.echo("  No optimizations possible.")
1169            else:
1170                click.echo(
1171                    f"  Optimized from {len(operations)} operations to {len(new_operations)} operations."
1172                )
1173
1174    # Work out the value of replaces (any squashed ones we're re-squashing)
1175    # need to feed their replaces into ours
1176    replaces = []
1177    for migration in migrations_to_squash:
1178        if migration.replaces:
1179            replaces.extend(migration.replaces)
1180        else:
1181            replaces.append((migration.package_label, migration.name))
1182
1183    # Make a new migration with those operations
1184    subclass = type(
1185        "Migration",
1186        (migrations.Migration,),
1187        {
1188            "dependencies": dependencies,
1189            "operations": new_operations,
1190            "replaces": replaces,
1191        },
1192    )
1193    if start_migration_name:
1194        if squashed_name:
1195            # Use the name from --squashed-name
1196            prefix, _ = start_migration.name.split("_", 1)
1197            name = f"{prefix}_{squashed_name}"
1198        else:
1199            # Generate a name
1200            name = f"{start_migration.name}_squashed_{migration.name}"
1201        new_migration = subclass(name, package_label)
1202    else:
1203        name = f"0001_{'squashed_' + migration.name if not squashed_name else squashed_name}"
1204        new_migration = subclass(name, package_label)
1205        new_migration.initial = True
1206
1207    # Write out the new migration file
1208    writer = MigrationWriter(new_migration)
1209    if os.path.exists(writer.path):
1210        raise click.ClickException(
1211            f"Migration {new_migration.name} already exists. Use a different name."
1212        )
1213    with open(writer.path, "w", encoding="utf-8") as fh:
1214        fh.write(writer.as_string())
1215
1216    if verbosity > 0:
1217        click.secho(
1218            f"Created new squashed migration {writer.path}", fg="green", bold=True
1219        )
1220        click.echo(
1221            "  You should commit this migration but leave the old ones in place;\n"
1222            "  the new migration will be used for new installs. Once you are sure\n"
1223            "  all instances of the codebase have applied the migrations you squashed,\n"
1224            "  you can delete them."
1225        )
1226        if writer.needs_manual_porting:
1227            click.secho("Manual porting required", fg="yellow", bold=True)
1228            click.echo(
1229                "  Your migrations contained functions that must be manually copied over,\n"
1230                "  as we could not safely copy their implementation.\n"
1231                "  See the comment at the top of the squashed migration for details."
1232            )