{"id":50525560,"url":"https://github.com/jakub-bacic/sqlalchemy-deprecated-column","last_synced_at":"2026-06-03T07:31:29.797Z","repository":{"id":351786001,"uuid":"1212569366","full_name":"jakub-bacic/sqlalchemy-deprecated-column","owner":"jakub-bacic","description":"Utility to mark SQLAlchemy ORM columns as deprecated to allow removing them in a backwards compatible manner.","archived":false,"fork":false,"pushed_at":"2026-05-13T23:33:51.000Z","size":224,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-14T01:40:20.943Z","etag":null,"topics":["backward-compatibility","migrations","python","sqlalchemy","sqlalchemy-database-migrations"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jakub-bacic.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-16T14:05:40.000Z","updated_at":"2026-05-12T13:01:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jakub-bacic/sqlalchemy-deprecated-column","commit_stats":null,"previous_names":["jakub-bacic/sqlalchemy-deprecated-column"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/jakub-bacic/sqlalchemy-deprecated-column","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jakub-bacic%2Fsqlalchemy-deprecated-column","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jakub-bacic%2Fsqlalchemy-deprecated-column/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jakub-bacic%2Fsqlalchemy-deprecated-column/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jakub-bacic%2Fsqlalchemy-deprecated-column/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jakub-bacic","download_url":"https://codeload.github.com/jakub-bacic/sqlalchemy-deprecated-column/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jakub-bacic%2Fsqlalchemy-deprecated-column/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33853991,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backward-compatibility","migrations","python","sqlalchemy","sqlalchemy-database-migrations"],"created_at":"2026-06-03T07:31:25.708Z","updated_at":"2026-06-03T07:31:29.790Z","avatar_url":"https://github.com/jakub-bacic.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sqlalchemy-deprecated-column\n\n[![PyPi](https://img.shields.io/pypi/v/sqlalchemy-deprecated-column.svg)](https://pypi.python.org/pypi/sqlalchemy-deprecated-column/)\n[![PyVersions](https://img.shields.io/pypi/pyversions/sqlalchemy-deprecated-column.svg)](https://pypi.python.org/pypi/sqlalchemy-deprecated-column/)\n[![Coverage](https://codecov.io/gh/jakub-bacic/sqlalchemy-deprecated-column/graph/badge.svg)](https://codecov.io/gh/jakub-bacic/sqlalchemy-deprecated-column)\n[![License](https://img.shields.io/github/license/jakub-bacic/sqlalchemy-deprecated-column.svg)](https://github.com/jakub-bacic/sqlalchemy-deprecated-column/blob/main/LICENSE)\n\nSafely remove SQLAlchemy columns through a gradual deprecation process.\nInspired by [django-deprecate-fields](https://github.com/3YOURMIND/django-deprecate-fields).\n\n## Installation\n\n```bash\npip install sqlalchemy-deprecated-column\n```\n\nWhile the project is pre-1.0, breaking changes may land in **minor** releases (e.g. `0.1.x` → `0.2.0`), following the SemVer convention for `0.y.z` versions. Patch releases (`0.1.3` → `0.1.4`) remain backwards-compatible. To avoid surprise breakage, pin to a specific minor version.\n\n## How it works\n\nRemoving a column from a live database requires coordination between code and schema changes. Dropping the column in a single step would break any running application instances that still reference it. This library lets you do this safely in three steps:\n\n1. **Deprecate**: replace `mapped_column()` with `deprecated_column()` (ORM) or `Column()` with `DeprecatedColumn()` (Core) and run an Alembic migration. The column stays in the database but becomes nullable if it wasn't already.\n2. **Deploy**: the column is hidden from all generated SQL. Any remaining code that references the column gets a `DeprecationWarning` at runtime (or raises `ColumnDeprecatedError` if `raise_on_access=True`), making stale references easy to find and remove.\n3. **Remove**: once all references are gone, delete the deprecated column definition and run a final migration to drop the column from the database.\n\n## Usage\n\n### ORM\n\n`deprecated_column()` is a drop-in replacement for `mapped_column()` and accepts the same arguments. For columns declared with only a bare `Mapped[T]` annotation, add `deprecated_column()` with no arguments.\n\n**Before:**\n\n```python\nclass User(Base):\n    ...\n    old_username: Mapped[str] = mapped_column(String(200), index=True)\n    old_email: Mapped[str]\n```\n\n**After:**\n\n```python\nfrom sqlalchemy_deprecated_column import deprecated_column\n\nclass User(Base):\n    ...\n    old_username: Mapped[str] = deprecated_column(String(200), index=True)\n    old_email: Mapped[str] = deprecated_column()\n```\n\nWhile the column is deprecated the library:\n\n- **Hides it from the ORM**: the column is excluded from all generated SQL queries — existing application code stays compatible even after the column is eventually dropped from the database.\n- **Warns on instance read**: `instance.old_username` returns `None` and emits a `DeprecationWarning` naming the model and column, so the call site is easy to locate.\n- **Warns on class-level reference**: `User.old_username` (e.g. in filter expressions) emits a `DeprecationWarning` and evaluates to SQL `NULL`.\n- **Warns on write and discards the value**: `instance.old_username = \"x\"` emits a `DeprecationWarning` and silently drops the value, so no stale data is written to the database.\n\n### Core (experimental)\n\n\u003e [!NOTE]\n\u003e This feature uses SQLAlchemy internal APIs — Core offers no public hooks for intercepting column references, unlike the ORM.\n\n`DeprecatedColumn()` is a drop-in replacement for `Column()` in Core `Table` definitions and accepts the same arguments.\n\n**Before:**\n\n```python\nfrom sqlalchemy import Column, String, Table, MetaData\n\nmetadata = MetaData()\nusers = Table(\n    \"users\",\n    metadata,\n    # other columns...\n    Column(\"old_username\", String(200)),\n)\n```\n\n**After:**\n\n```python\nfrom sqlalchemy import Column, String, Table, MetaData\nfrom sqlalchemy_deprecated_column import DeprecatedColumn\n\nmetadata = MetaData()\nusers = Table(\n    \"users\",\n    metadata,\n    # other columns...\n    DeprecatedColumn(\"old_username\", String(200)),\n)\n```\n\nWhile the column is deprecated the library:\n\n- **Projects it as NULL in SELECT**: `select(users)` substitutes `NULL` for the column — result rows remain accessible (`row.old_username` returns `None`), but no actual data is fetched from the database.\n- **Warns on explicit SELECT**: `select(users.c.old_username)` emits a `DeprecationWarning` at compile time and substitutes `NULL` in the query, so no data is fetched from the database.\n- **Warns on WHERE reference**: using the column in a filter expression (e.g. `users.c.old_username == \"x\"`) emits a `DeprecationWarning` at expression-build time and substitutes `NULL`.\n- **Hides it from auto-INSERT/UPDATE**: when the column is not explicitly referenced, it is excluded from generated INSERT and UPDATE statements entirely.\n- **Warns on explicit INSERT/UPDATE**: explicitly passing the column in `.values()` emits a `DeprecationWarning` and substitutes `NULL` for the supplied value.\n\n\u003e [!IMPORTANT]\n\u003e When a deprecated column is explicitly passed to `.values()`, only the supplied value is replaced with `NULL` — the column name cannot be removed from the generated SQL. Use the `DeprecationWarning` to locate and remove the explicit reference.\n\n## Options\n\n### raise_on_access\n\nPass `raise_on_access=True` to raise a `ColumnDeprecatedError` on any access instead of emitting a warning. This works for both ORM and Core and is useful when you want to enforce that all stale references are removed — for example, by letting the exception surface in tests or CI:\n\n```python\n# ORM (inside a model class)\nold_username: Mapped[str] = deprecated_column(String(200), raise_on_access=True)\n\n# Core (inside a Table definition)\nDeprecatedColumn(\"old_username\", String(200), raise_on_access=True)\n```\n\n## Alembic integration\n\nAdd the following **at the top of `alembic/env.py`, before any model or metadata imports**:\n\n```python\nimport sqlalchemy_deprecated_column\nsqlalchemy_deprecated_column.configure(alembic_mode=True)\n```\n\nIn Alembic mode, `deprecated_column()` acts as `mapped_column(nullable=True)` and `DeprecatedColumn()` acts as `Column(nullable=True)`. Alembic will:\n\n- **Not** generate `DROP COLUMN` for deprecated columns.\n- **Generate `ALTER TABLE … DROP NOT NULL`** if the column was originally non-nullable. This is needed because once the column is deprecated it is no longer included in `INSERT` statements — a `NOT NULL` column without a value would cause those inserts to fail.\n\n## Requirements\n\n- Python 3.10+\n- SQLAlchemy 2.0+\n\n## License\n\nThe code in this project is licensed under MIT license. See [LICENSE](./LICENSE) for more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjakub-bacic%2Fsqlalchemy-deprecated-column","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjakub-bacic%2Fsqlalchemy-deprecated-column","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjakub-bacic%2Fsqlalchemy-deprecated-column/lists"}