Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ac6289b
Add tests and support for enum names which are camel case. Fixes: #11…
Apteryx0 Apr 17, 2025
6526649
Extract SQLAlchemy module prefix from Alembic configuration
Jazzinghen Apr 23, 2025
2b4cbd0
Add support for Alembic module prefix as well
Jazzinghen Apr 24, 2025
36882af
Add custom migration context support to tests
Jazzinghen Apr 24, 2025
b3df107
Add test case for custom context enum creation
Jazzinghen Apr 24, 2025
df4ec79
Fixes: #112 - Added docstring to migration context generation
Jazzinghen Apr 24, 2025
90197e3
Fixes: #112 - Added test for Sync functionality
Jazzinghen Apr 24, 2025
e2be65f
Use uv instead of poetry
RustyGuard Jul 20, 2025
6500099
Install uv step in GitHub workflow
RustyGuard Jul 20, 2025
c73b625
`pytest` -> `uv run pytest` in GitHub workflow
RustyGuard Jul 20, 2025
f25a91f
lint
RustyGuard Jul 20, 2025
6bc1b63
update sigstore/gh-action-sigstore-python to v3.0.1
RustyGuard Jul 20, 2025
fae99a8
Fixes: #103
jonfinerty Sep 1, 2025
2adf51a
Merge pull request #122 from jonfinerty/partial_index_support
RustyGuard Oct 11, 2025
f552336
Add black as dev dependency
RustyGuard Oct 11, 2025
80aef75
Fix black at 25.1.0 version
RustyGuard Oct 11, 2025
ab638b7
Fix black at 25.9.0 version
RustyGuard Oct 11, 2025
c75a0a5
Up actions/checkout to v5
RustyGuard Oct 11, 2025
1c00239
Merge branch 'develop' into move-to-uv
RustyGuard Oct 11, 2025
8c69b79
Reformat
RustyGuard Oct 11, 2025
ad71fba
Use optional instead of | for backwards compatibility
RustyGuard Oct 11, 2025
af1a602
Reformat
RustyGuard Oct 11, 2025
86ab01f
Use optional instead of | for backwards compatibility
RustyGuard Oct 11, 2025
f37702e
Fixup tests cleanup
RustyGuard Oct 11, 2025
208d212
Fixup tests cleanup
RustyGuard Oct 11, 2025
8ff4efd
Reformat
RustyGuard Oct 11, 2025
08fd900
Merge branch 'develop' into move-to-uv
RustyGuard Oct 11, 2025
3683103
Merge branch 'develop' into camel_case_enum_names
RustyGuard Oct 11, 2025
d4870ee
Merge pull request #111 from Apteryx0/camel_case_enum_names
RustyGuard Oct 11, 2025
ea55c8e
Reformat
RustyGuard Oct 11, 2025
5454796
Fixup create_comparison_operators
RustyGuard Oct 11, 2025
07e8ea0
Merge branch 'develop' into jazzinghen/use_alembic_sqlalchemy_prefix
RustyGuard Oct 11, 2025
f3dacaa
Merge pull request #113 from Jazzinghen/jazzinghen/use_alembic_sqlalc…
RustyGuard Oct 11, 2025
d65392c
Fixup usage of dict type
RustyGuard Oct 11, 2025
56c97fe
Merge branch 'develop' into move-to-uv
RustyGuard Oct 11, 2025
0fd7b1d
Mark Python 3.14 as supported
RustyGuard Oct 11, 2025
da83ba2
Reformat
RustyGuard Oct 11, 2025
652a07d
Merge pull request #117 from Pogchamp-company/move-to-uv
RustyGuard Oct 11, 2025
57665f7
Bump version to 1.9.0b1
RustyGuard Oct 11, 2025
739cfe6
Bump version to 1.9.0
RustyGuard Oct 11, 2025
a9cf963
Clarify that library does not affect old migrations. Fixes: #114
RustyGuard Oct 11, 2025
d292306
update uv.lock
RustyGuard Oct 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/black.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: psf/black@25.1.0
- uses: actions/checkout@v5
- uses: psf/black@25.9.0
6 changes: 3 additions & 3 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v4
with:
Expand Down Expand Up @@ -72,9 +72,9 @@ jobs:
name: python-package-distributions
path: dist/
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v1.2.3
uses: sigstore/gh-action-sigstore-python@v3.0.1
with:
inputs: >-
inputs: |
./dist/*.tar.gz
./dist/*.whl
- name: Upload artifact signatures to GitHub Release
Expand Down
17 changes: 9 additions & 8 deletions .github/workflows/test_on_push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ jobs:
runs-on: [ubuntu-22.04]
strategy:
matrix:
sqlalchemy: [ "1.4", "2.0" ]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
uv-group: [ "matrix-1-4", "matrix-2-0" ]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]

# Service containers to run with `container-job`
services:
Expand All @@ -40,17 +40,18 @@ jobs:
--health-retries 5

steps:
- uses: actions/checkout@master
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "0.8.0"
- name: Install requirements
# Устанавливаем зависимости
run: pip install -r tests/requirements.txt
- name: Install explicit sqlalchemy
run: pip install sqlalchemy~=${{ matrix.sqlalchemy }}
run: uv sync --locked --group=${{matrix.uv-group}}
- name: Run tests
run: pytest
run: uv run pytest
env:
DATABASE_URI: postgresql://postgres:postgres@localhost:5432/postgres
8 changes: 6 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ FROM python:latest

COPY ./alembic_postgresql_enum ./alembic_postgresql_enum
COPY ./tests ./tests
COPY ./pyproject.toml ./pyproject.toml
COPY ./uv.lock ./uv.lock
COPY ./README.md ./README.md

WORKDIR ./tests

RUN pip install -r requirements.txt
COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /uvx /bin/
RUN uv sync --group matrix-2-0

ENTRYPOINT pytest
ENTRYPOINT uv run pytest
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import alembic_postgresql_enum

To the top of your migrations/env.py file.

This import will affect newly generated migrations.
To try it out you can edit some enums in your schema
and then run `alembic revision --autogenerate`

## Features

* [Creation of enums](#creation-of-enum)
Expand All @@ -38,6 +42,7 @@ To the top of your migrations/env.py file.
* [Creation of new enum values](#creation-of-new-enum-values)
* [Deletion of enums values](#deletion-of-enums-values)
* [Renaming of enum values](#rename-enum-value)
* [Partial index preservation](#partial-index-preservation)
* [Omitting managing enums](#omitting-managing-enums)

## Creation of enum<a id="creation-of-enum"></a>
Expand Down Expand Up @@ -277,6 +282,86 @@ Do not forget to switch places old and new values for downgrade

All defaults in postgres will be renamed automatically as well

## Partial index preservation<a id="partial-index-preservation"></a>

When modifying enum values, partial indexes that reference the enum type are preserved via dropping and recreating. This is particularly important for indexes with `WHERE` clauses that use enum comparisons. Depending on the size and complexity of the index this might impact the speed and locking nature of the schema migration.

**Note:** For alembic's offline mode support, partial index detection happens during migration generation time. The detected indexes are then passed to the migration as a parameter. This ensures that offline migrations can execute without needing database access.

### Example Scenario

Consider a table with a partial unique index:

```python
class UserStatus(enum.Enum):
active = "active"
deleted = "deleted"

class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
username = Column(String, nullable=False)
status = Column(postgresql.ENUM(UserStatus))

__table_args__ = (
Index(
"uq_user_username",
"username",
unique=True,
postgresql_where=(text("status != 'deleted'")),
),
)
```

When you add a new enum value (e.g., `pending`), the library will:

1. **Detect** any indexes that reference the enum type in their WHERE clauses (during migration generation)
2. **Temporarily drop** these indexes before modifying the enum
3. **Recreate** the indexes with their original definitions after the enum modification is complete

In the generated migration, indexes are included as a parameter:

```python
from alembic_postgresql_enum.sql_commands.indexes import TableIndex

op.sync_enum_values(
enum_schema='public',
enum_name='userstatus',
new_values=['active', 'pending', 'deleted'],
affected_columns=[TableReference(...)],
enum_values_to_rename=[],
indexes_to_recreate=[
TableIndex(
name='uq_user_username',
definition="CREATE UNIQUE INDEX uq_user_username ON users USING btree (username) WHERE (status <> 'deleted'::userstatus)",
),
],
)
```

This ensures that partial indexes like `WHERE status != 'deleted'` continue to work correctly after enum modifications, without manual intervention.

### What Gets Preserved

- Partial indexes with WHERE clauses referencing the enum
- Unique constraints with partial conditions
- Any index using enum comparisons (`=`, `!=`, `IN`, etc.)
- When enum values are renamed (not dropped), the index definitions are updated to use the new value names.

### Handling Dropped Enum Values

When an enum value referenced in a partial index is being dropped, the library will detect this and provide a clear error message:

```
ERROR: Cannot drop enum value(s) 'deleted' because they are referenced in partial index 'idx_users'
Index definition: CREATE INDEX idx_users ON users WHERE (status != 'deleted'::user_status)

To resolve this issue, either:
1. Use enum_values_to_rename to rename 'deleted' to other values instead of dropping
2. Manually drop the index 'idx_users' before running this migration
3. Update your code to not drop these enum values
```

## Omitting managing enums<a id="omitting-managing-enums"></a>

If configured `include_name` function returns `False` given enum will be not managed.
Expand Down
1 change: 1 addition & 0 deletions alembic_postgresql_enum/compare_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,5 @@ def compare_enums(
declarations.enum_table_references,
schema,
upgrade_ops,
connection=autogen_context.connection,
)
11 changes: 11 additions & 0 deletions alembic_postgresql_enum/detection_of_changes/enum_alteration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import logging

from typing import TYPE_CHECKING

from alembic.operations.ops import UpgradeOps

from alembic_postgresql_enum.get_enum_data import (
Expand All @@ -14,6 +16,10 @@
)
from alembic_postgresql_enum.operations.sync_enum_values import SyncEnumValuesOp
from alembic_postgresql_enum.configuration import get_configuration
from alembic_postgresql_enum.sql_commands.indexes import get_dependent_indexes

if TYPE_CHECKING:
from sqlalchemy.engine import Connection

log = logging.getLogger(f"alembic.{__name__}")

Expand All @@ -24,6 +30,7 @@ def sync_changed_enums(
table_references: EnumNamesToTableReferences,
schema: str,
upgrade_ops: UpgradeOps,
connection: "Connection",
):
configuration = get_configuration()

Expand All @@ -50,6 +57,9 @@ def sync_changed_enums(
list(new_values),
)
affected_columns = table_references[enum_name]

affected_indexes = get_dependent_indexes(connection, schema, enum_name)

op = SyncEnumValuesOp(
schema,
enum_name,
Expand All @@ -59,5 +69,6 @@ def sync_changed_enums(
affected_columns,
key=lambda reference: (reference.table_schema, reference.table_name, reference.column_name),
),
sorted(affected_indexes, key=lambda index: index.name),
)
upgrade_ops.ops.append(op)
6 changes: 4 additions & 2 deletions alembic_postgresql_enum/operations/create_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ def reverse(self):
@alembic.autogenerate.render.renderers.dispatch_for(CreateEnumOp)
def render_create_enum_op(autogen_context: AutogenContext, op: CreateEnumOp):
assert autogen_context.dialect is not None
sqlalchemy_module_prefix = autogen_context.opts.get("sqlalchemy_module_prefix", "sa.")
alembic_module_prefix = autogen_context.opts.get("alembic_module_prefix", "op.")
if op.schema != autogen_context.dialect.default_schema_name:
return f"""
sa.Enum({', '.join(map(repr, op.enum_values))}, name='{op.name}', schema='{op.schema}').create(op.get_bind())
{sqlalchemy_module_prefix}Enum({', '.join(map(repr, op.enum_values))}, name='{op.name}', schema='{op.schema}').create({alembic_module_prefix}get_bind())
""".strip()

return f"""
sa.Enum({', '.join(map(repr, op.enum_values))}, name='{op.name}').create(op.get_bind())
{sqlalchemy_module_prefix}Enum({', '.join(map(repr, op.enum_values))}, name='{op.name}').create({alembic_module_prefix}get_bind())
""".strip()
6 changes: 4 additions & 2 deletions alembic_postgresql_enum/operations/drop_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ def reverse(self):
@alembic.autogenerate.render.renderers.dispatch_for(DropEnumOp)
def render_drop_enum_op(autogen_context: AutogenContext, op: DropEnumOp):
assert autogen_context.dialect is not None
sqlalchemy_module_prefix = autogen_context.opts.get("sqlalchemy_module_prefix", "sa.")
alembic_module_prefix = autogen_context.opts.get("alembic_module_prefix", "op.")
if op.schema != autogen_context.dialect.default_schema_name:
return f"""
sa.Enum({', '.join(map(repr, op.enum_values))}, name='{op.name}', schema='{op.schema}').drop(op.get_bind())
{sqlalchemy_module_prefix}Enum({', '.join(map(repr, op.enum_values))}, name='{op.name}', schema='{op.schema}').drop({alembic_module_prefix}get_bind())
""".strip()

return f"""
sa.Enum({', '.join(map(repr, op.enum_values))}, name='{op.name}').drop(op.get_bind())
{sqlalchemy_module_prefix}Enum({', '.join(map(repr, op.enum_values))}, name='{op.name}').drop({alembic_module_prefix}get_bind())
""".strip()
Loading