Skip to main content

Educationwarehouse's Migrate

PyPI - Version PyPI - Python Version


Table of Contents

Installation

pip install edwh-migrate
# or to include extra dependencies (psycopg2, redis):
pip install edwh-migrate[full]

Documentation

Config: Environment variables

These variables can be set in the current environment or via .env:

  • MIGRATE_URI (required): regular postgres://user:password@host:port/database or sqlite:///path/to/database URI. DB_URI can be used as an alias.
  • MIGRATIONS_FILE (optional): path to a file containing migrations. By default, a file called migrations.py in the working directory is used.
  • DATABASE_TO_RESTORE: path to a (compressed) SQL file to restore when the migrations table is missing. .xz,.gz and .sql are supported, for both postgres (via psql) and sqlite (via sqlite3). Defaults to /data/database_to_restore.sql.
  • MIGRATE_CAT_COMMAND: for unsupported compression formats, this command decompresses the file and produces sql on the stdout.
  • SCHEMA_VERSION: Used in case of schema versioning. Set by another process. See Usage for the lock file mechanics.
  • REDIS_HOST: If set, all keys of the redis database 0 will be removed after a successful run. A full redis://host:port URL is also accepted.
  • MIGRATE_TABLE: name of the table where installed migrations are stored. Defaults to ewh_implemented_features.
  • FLAG_LOCATION: when using schema versioned lock files, this directory is used to store the flags. Defaults to /flags.
  • CREATE_FLAG_LOCATION (bool): should the directory above be created if it does not exist yet? Defaults to 0 (false).
  • SCHEMA: (for postgres) set the default namespace (search_path). Defaults to public. Set to false to leave the search path untouched.
  • USE_TYPEDAL: pass a TypeDAL instance to migrations instead of a regular pyDAL.
  • DB_FOLDER: directory where pyDAL stores its .table files and sql logs. Uses the pyDAL default when unset.
  • MIGRATION_ORDERING_MODE: ordering strategy for migrations. legacy (default) keeps definition/import order; intertwined interleaves migrations from multiple files by trailing numeric suffix (e.g. ..._20260330_001). See Intertwined ordering.

Config: pyproject.toml

You can also set your config variables via the [tool.migrate] key in pyproject.toml. First, these variables are loaded and then updated with variables from the environment. This way, you can set static variables (the ones you want in git, e.g. the migrate_table name or path to the backup to restore) in the toml, and keep private/dynamic vars in the environment (e.g. the database uri or schema version).

Keys are case-insensitive and - and _ are interchangeable (migrate-uri = migrate_uri = MIGRATE_URI).

Example:

[tool.migrate]
migrate_uri = "" # filled in by .env
database-to-restore = "migrate/data/db_backup.sql"
# ...

Creating a migrations.py

from pydal import DAL

from edwh_migrate import migration


@migration
def feature_1(db):
    print("feature_1")
    return True


@migration(requires=[feature_1])  # optional `requires` ensures previous migration(s) are installed
def functionalname_date_sequencenr(db: DAL):
    db.executesql("""
        CREATE TABLE ...
    """)
    db.commit()
    return True

Dependencies: requires

requires accepts a single function, a single migration name, or a (mixed) list of both:

@migration(requires=feature_1)            # a single function
@migration(requires="feature_1")          # a single migration name
@migration(requires=[feature_1, "other"]) # or a (mixed) list

Return values

A migration function must return a boolean:

  • True: the migration is committed and marked as installed.
  • False: the migration is rolled back and marked as failed. It will be retried on the next run.
  • If an exception is raised, the whole run aborts with exit code 1 (and the lock file, when used, is removed so the run can be retried).

Splitting migrations across multiple files

Larger projects can split migrations over multiple files. Import the other files from your main migrations file; importing a file registers its migrations:

# migrations.py
import users_migrations  # noqa: F401 -- importing registers the migrations
import billing_migrations  # noqa: F401

The final run order is resolved topologically across all imported files:

  • migrations with requires run as soon as all their requirements have run;
  • independent migrations keep their definition/import order (legacy) or interleave by date suffix (intertwined);
  • requiring an unknown migration raises ValueError: ... depends on ... which is unknown;
  • circular dependencies raise ValueError: circular dependency detected among migrations: ....

Usage

When your configuration is set up properly and you have a file containing your migrations, you can simply run:

migrate                            # run ./migrations.py (or MIGRATIONS_FILE)
migrate path/to/my_migrations.py   # use a different file
migrate path/to/folder             # use path/to/folder/migrations.py
migrate --list                     # or -l: show a status table (succeeded / failed / missing)
migrate --help                     # or -h: show usage information

Lock files

When SCHEMA_VERSION is set, a lock file migrate-{SCHEMA_VERSION}.complete is created in FLAG_LOCATION after a successful run:

  • if the lock file already exists, nothing is run and migrate exits with code 0;
  • on failure the lock file is removed, so the migration will be retried;
  • failed migrations (installed = false) are also retried on the next run.

Advanced Topics

Intertwined ordering across multiple files

When migrations are split over multiple files (e.g. one per app or domain), legacy ordering runs them in import order: everything from the first file, then everything from the second. With MIGRATION_ORDERING_MODE=intertwined, independent migrations are interleaved by their trailing numeric suffix instead:

# users_migrations.py
@migration
def users_create_table_20260330_001(db): ...


@migration
def users_add_email_20260402_001(db): ...


# billing_migrations.py
@migration
def billing_create_table_20260331_001(db): ...

Resulting run order with intertwined:

  1. users_create_table_20260330_001
  2. billing_create_table_20260331_001
  3. users_add_email_20260402_001

With legacy the order would have been: both users_* migrations first, then billing_create_table_20260331_001.

Notes:

  • the suffix convention is _<date>_<sequence> (e.g. _20260330_001), but any trailing _number or _number_number works;
  • migrations without a numeric suffix are treated as suffix 0_0, so they run first;
  • migrations with requires always run as soon as their requirements are satisfied, regardless of suffix.

Using ViewMigrationManager via Subclasses

ViewMigrationManager is designed to manage the lifecycle of view migrations in a database using context management. It ensures that migrations are properly handled with dependencies between different migrations.

Usage

  1. Define Subclasses: Create subclasses of ViewMigrationManager and implement the required methods up and down.

    from edwh_migrate import ViewMigrationManager
    
    
    class MyExampleView_V1(ViewMigrationManager):
        # Define dependencies (optional)
        uses = ()
        # Specify a migration that must have run before this class may be used
        since = "previous_migration"
    
        def up(self):
            # Logic to apply the migration
            self.db.executesql(
                """
                CREATE MATERIALIZED VIEW my_example_view AS
                SELECT id, name FROM my_table;
                """
            )
    
        def down(self):
            # Logic to reverse the migration
            self.db.executesql(
                """
                DROP MATERIALIZED VIEW IF EXISTS my_example_view;
                """
            )
    
    
    class AnotherExampleView(ViewMigrationManager):
        # This class depends on MyExampleView_V1
        uses = (MyExampleView_V1,)
    
        def up(self):
            # Logic to apply the migration
            self.db.executesql(
                """
                CREATE MATERIALIZED VIEW another_example_view AS
                SELECT id, name FROM my_example_view;
                """
            )
    
        def down(self):
            # Logic to reverse the migration
            self.db.executesql(
                """
                DROP MATERIALIZED VIEW IF EXISTS another_example_view;
                """
            )
    
  2. Define the previous_migration: Create a migration function that serves as the prerequisite for MyExampleView_V1.

    from edwh_migrate import migration
    
    
    @migration
    def previous_migration(db):
        db.executesql("""
        CREATE TABLE my_table (
            id SERIAL PRIMARY KEY,
            name VARCHAR(255)
        );
        """)
        db.commit()
        return True
    
  3. Use the Subclass in a Migration Function: Utilize the subclass in a migration function to manage the view migration context.

    from edwh_migrate import migration
    
    
    @migration
    def upgrade_some_source_table_that_my_example_view_depends_on(db):
        with MyExampleView_V1(db):
            db.executesql("""
            ALTER TABLE my_table
            ADD COLUMN new_column VARCHAR(255);
            """)
        db.commit()
        return True
    

In the example above:

  • MyExampleView_V1 is a subclass of ViewMigrationManager that manages the lifecycle of a materialized view named my_example_view.
  • AnotherExampleView is another subclass that depends on MyExampleView_V1 and manages the lifecycle of another materialized view named another_example_view.
  • The up method in MyExampleView_V1 contains the logic to create the materialized view my_example_view.
  • The down method in MyExampleView_V1 contains the logic to drop the materialized view my_example_view.
  • The up method in AnotherExampleView contains the logic to create the materialized view another_example_view that references my_example_view.
  • The down method in AnotherExampleView contains the logic to drop the materialized view another_example_view.
  • The since attribute specifies that a particular migration (previous_migration) must have run before MyExampleView_V1 may be used.
  • The previous_migration function creates the table my_table and serves as a prerequisite for MyExampleView_V1.
  • The migration decorator is used to define a migration function (upgrade_some_source_table_that_my_example_view_depends_on) that executes within the context of MyExampleView_V1.
  • The with MyExampleView_V1(db) block ensures that the down method is called before the block executes and the up method is called after the block completes.

In addition to 'since', the inverse 'until' can also be used.

Library usage

Besides the CLI, edwh_migrate can be used as a library:

Export Description
migration Decorator to register a migration function.
migrations The MigrationStore singleton holding all registered migrations.
setup_db() Connect to the configured database and return a (Type)DAL instance.
activate_migrations() Run all pending migrations (without lock file handling).
list_migrations(config, args=None) Import (if needed) and list all registered migrations.
mark_migration(db, name, installed) Manually store a migration result in the features table.
recover_database_from_backup() Restore DATABASE_TO_RESTORE into the configured database.
get_config() / Config Access the resolved configuration.
console_hook(args, config=None) The CLI entry point; takes a list of arguments and returns an exit code instead of exiting.
ViewMigrationManager Base class for view migrations (see above).
InvalidConfigException, UnknownConfigException Configuration error types.
from edwh_migrate import console_hook, get_config, list_migrations

config = get_config()
print(list_migrations(config))  # OrderedDict of registered migrations

exit_code = console_hook([], config)  # run the migrations, 0 on success

License

edwh-migrate is distributed under the terms of the MIT license.

Metadata

Release files for edwh-migrate 2.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for edwh-migrate 2.0.2
File Size Uploaded
edwh_migrate-2.0.2.tar.gz 22.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for edwh-migrate 2.0.2
File Interpreter ABI Platform
edwh_migrate-2.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 46.5 kB

Release files / edwh_migrate-2.0.2.tar.gz

Download URL edwh_migrate-2.0.2.tar.gz
Size 22.8 kB
Tags Source
SHA-256 checksum
How to use checksums
1163f77e11214c21c9f1c6d1c53a38f25f34b48ba9fd9dc686e1577743f4ef8f
BLAKE2b-256 checksum
How to use checksums
42a8663f91b278634d3613dd36a69eb5417f508d3fb146646c2e06c71c114aaf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / edwh_migrate-2.0.2-py3-none-any.whl

Download URL edwh_migrate-2.0.2-py3-none-any.whl
Size 23.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
35469be2533b59a9cb9ff1d154773a9445e0d4f4a795dc4396a7c60781b734b1
BLAKE2b-256 checksum
How to use checksums
30b890976cf71c7383d33db416e9efcd5fc1ee796634c3252c50d2a4076bffc6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

2.0.3

2 release files

This release

2.0.2 This release

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.2.0

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.10.4

2 release files

0.9.9

2 release files

0.9.8

2 release files

0.9.7

2 release files

0.9.6

2 release files

0.9.5

2 release files

0.9.4

2 release files

0.9.3

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.5

2 release files

0.7.4

2 release files

0.7.3

2 release files

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page