sqlalchemy-deprecated-column 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlalchemy_deprecated_column/__init__.py +3 -0
- sqlalchemy_deprecated_column/deprecate_field.py +88 -0
- sqlalchemy_deprecated_column/py.typed +0 -0
- sqlalchemy_deprecated_column-0.1.0.dist-info/METADATA +95 -0
- sqlalchemy_deprecated_column-0.1.0.dist-info/RECORD +7 -0
- sqlalchemy_deprecated_column-0.1.0.dist-info/WHEEL +4 -0
- sqlalchemy_deprecated_column-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import null
|
|
8
|
+
from sqlalchemy.ext.hybrid import hybrid_property
|
|
9
|
+
from sqlalchemy.orm import mapped_column
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class _Configuration:
|
|
14
|
+
alembic_mode: bool = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_config = _Configuration()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def configure(alembic_mode: bool = False) -> None:
|
|
21
|
+
"""Configure sqlalchemy-deprecate-fields behaviour.
|
|
22
|
+
|
|
23
|
+
Call with ``alembic_mode=True`` at the top of ``alembic/env.py``, before
|
|
24
|
+
any model imports, so that Alembic sees deprecated columns as real nullable
|
|
25
|
+
columns and does not generate DROP COLUMN migrations.
|
|
26
|
+
"""
|
|
27
|
+
_config.alembic_mode = alembic_mode
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _DeprecatedColumn:
|
|
31
|
+
def __init__(self, *_args: Any, **_kwargs: Any) -> None:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
35
|
+
@hybrid_property
|
|
36
|
+
def prop(instance: Any) -> None:
|
|
37
|
+
warnings.warn(
|
|
38
|
+
f"accessing deprecated field {type(instance).__name__}.{name}",
|
|
39
|
+
DeprecationWarning,
|
|
40
|
+
stacklevel=2,
|
|
41
|
+
)
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
@prop.inplace.setter
|
|
45
|
+
def _(instance: Any, value: Any) -> None:
|
|
46
|
+
warnings.warn(
|
|
47
|
+
f"writing to deprecated field {type(instance).__name__}.{name}",
|
|
48
|
+
DeprecationWarning,
|
|
49
|
+
stacklevel=2,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@prop.inplace.expression
|
|
53
|
+
@classmethod
|
|
54
|
+
def _(cls: type) -> Any:
|
|
55
|
+
warnings.warn(
|
|
56
|
+
f"referencing deprecated class field {cls.__name__}.{name}",
|
|
57
|
+
DeprecationWarning,
|
|
58
|
+
stacklevel=2,
|
|
59
|
+
)
|
|
60
|
+
return null()
|
|
61
|
+
|
|
62
|
+
setattr(owner, name, prop)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def deprecated_column(*args: Any, **kwargs: Any) -> Any:
|
|
66
|
+
"""Drop-in replacement for mapped_column() that marks the column as deprecated.
|
|
67
|
+
|
|
68
|
+
In normal mode the column is excluded from the ORM mapper entirely:
|
|
69
|
+
it will not appear in SELECT, INSERT, UPDATE, or RETURNING statements.
|
|
70
|
+
Instance reads return None and emit a DeprecationWarning; writes emit a
|
|
71
|
+
warning and are silently discarded.
|
|
72
|
+
|
|
73
|
+
In Alembic mode (after ``configure(alembic_mode=True)``) the call is
|
|
74
|
+
forwarded to ``mapped_column(*args, nullable=True, **kwargs)`` so that
|
|
75
|
+
Alembic sees the column as a regular nullable column and generates correct
|
|
76
|
+
migrations.
|
|
77
|
+
|
|
78
|
+
Usage::
|
|
79
|
+
|
|
80
|
+
class User(Base):
|
|
81
|
+
__tablename__ = "users"
|
|
82
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
83
|
+
old_email: Mapped[str] = deprecated_column(String(200))
|
|
84
|
+
"""
|
|
85
|
+
if _config.alembic_mode:
|
|
86
|
+
kwargs["nullable"] = True
|
|
87
|
+
return mapped_column(*args, **kwargs)
|
|
88
|
+
return _DeprecatedColumn(*args, **kwargs)
|
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-deprecated-column
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Utility to mark SQLAlchemy ORM columns as deprecated to allow removing them in a backwards compatible manner.
|
|
5
|
+
Author: Jakub Bacic
|
|
6
|
+
Author-email: Jakub Bacic <jakub.bacic@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Project-URL: Repository, https://github.com/jakub-bacic/sqlalchemy-deprecate-fields
|
|
25
|
+
Project-URL: Issues, https://github.com/jakub-bacic/sqlalchemy-deprecate-fields/issues
|
|
26
|
+
Project-URL: Changelog, https://github.com/jakub-bacic/sqlalchemy-deprecate-fields/blob/main/CHANGELOG.md
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# sqlalchemy-deprecated-column
|
|
30
|
+
|
|
31
|
+
Safely remove SQLAlchemy ORM columns through a gradual deprecation process.
|
|
32
|
+
Inspired by [django-deprecate-fields](https://github.com/3YOURMIND/django-deprecate-fields).
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install sqlalchemy-deprecated-column
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
Removing 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. `deprecated_column()` lets you do this safely in three steps:
|
|
43
|
+
|
|
44
|
+
1. **Deprecate**: replace `mapped_column()` with `deprecated_column()` and run an Alembic migration. The column stays in the database but becomes nullable if it wasn't already.
|
|
45
|
+
2. **Deploy**: the column is hidden from the ORM — it no longer appears in any generated SQL. Any remaining code that reads or writes the column gets a `DeprecationWarning` at runtime, making stale references easy to find and remove.
|
|
46
|
+
3. **Remove**: once all references are gone, delete the `deprecated_column()` definition from the model and run a final migration to drop the column from the database.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
Replace `mapped_column()` with `deprecated_column()` for any column you want to deprecate:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from sqlalchemy_deprecated_column import deprecated_column
|
|
54
|
+
|
|
55
|
+
class User(Base):
|
|
56
|
+
__tablename__ = "users"
|
|
57
|
+
|
|
58
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
59
|
+
email: Mapped[str] = mapped_column(String)
|
|
60
|
+
old_username: Mapped[str] = deprecated_column(String) # was: mapped_column(String)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
While the column is deprecated the library:
|
|
64
|
+
|
|
65
|
+
- **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.
|
|
66
|
+
- **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.
|
|
67
|
+
- **Warns on class-level reference**: `User.old_username` (e.g. in filter expressions) emits a `DeprecationWarning` and evaluates to SQL `NULL`.
|
|
68
|
+
- **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.
|
|
69
|
+
|
|
70
|
+
## Alembic integration
|
|
71
|
+
|
|
72
|
+
Add the following **at the top of `alembic/env.py`, before any model imports**:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import sqlalchemy_deprecated_column
|
|
76
|
+
sqlalchemy_deprecated_column.configure(alembic_mode=True)
|
|
77
|
+
|
|
78
|
+
# model imports must come after configure()
|
|
79
|
+
from myapp import mymodel
|
|
80
|
+
target_metadata = mymodel.Base.metadata
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
In Alembic mode, `deprecated_column()` acts as `mapped_column(nullable=True)`. Alembic will:
|
|
84
|
+
|
|
85
|
+
- **Not** generate `DROP COLUMN` for deprecated columns.
|
|
86
|
+
- **Generate `ALTER TABLE … DROP NOT NULL`** if the column was originally non-nullable. This is needed because once the column is deprecated the ORM stops including it in `INSERT` statements — a `NOT NULL` column without a value would cause those inserts to fail.
|
|
87
|
+
|
|
88
|
+
## Requirements
|
|
89
|
+
|
|
90
|
+
- Python 3.10+
|
|
91
|
+
- SQLAlchemy 2.0+
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
The code in this project is licensed under MIT license. See [LICENSE](./LICENSE) for more information.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
sqlalchemy_deprecated_column/__init__.py,sha256=zXxb7K26pIx6f36BbV0VBZVDEk9_wmwVQRhJZ9EKVc8,104
|
|
2
|
+
sqlalchemy_deprecated_column/deprecate_field.py,sha256=SirAUjWaluYb8eO8T4UJKVQI_eH7zGau-rgztyMmRWs,2758
|
|
3
|
+
sqlalchemy_deprecated_column/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
sqlalchemy_deprecated_column-0.1.0.dist-info/licenses/LICENSE,sha256=m77pqLLodhsaWr2RD7HxLjVDhci81XufaS719DWKrAI,1068
|
|
5
|
+
sqlalchemy_deprecated_column-0.1.0.dist-info/WHEEL,sha256=WvwXFgRajeoYkfRVmDhkP4Qlqo31Mk687zIO2QQoFmw,80
|
|
6
|
+
sqlalchemy_deprecated_column-0.1.0.dist-info/METADATA,sha256=rw8YdGSZcePNNKdz2O0TuqU7VlOk1fK3XdtgH7fO76s,4472
|
|
7
|
+
sqlalchemy_deprecated_column-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jakub Bacic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|