googleapis / googleapis/google-cloud-python
Support automatic translation of unique=True / UniqueConstraint to unique indexes in migrations
- 主要言語
- Python
- スター
- 5.4k
- フォーク
- 1.8k
- 平均マージ
- 3日 4時間
- マージ済み PR(30日)
- 122
説明
## **Is your feature request related to a problem? Please describe.**
Yes. And relates to googleapis/python-spanner-sqlalchemy#228
The Cloud Spanner SQLAlchemy dialect (`python-spanner-sqlalchemy`) does not support table-level `UNIQUE` constraints, while the idiomatic SQLAlchemy/SQLModel way to express uniqueness is `unique=True` on a `Column` or a `UniqueConstraint` in `__table_args__`.
When running Alembic autogenerate or migrations against Spanner, these become `CREATE UNIQUE CONSTRAINT` or `ADD CONSTRAINT ... UNIQUE` operations that fail, since Spanner does not support table-level unique constraints.
This is common for teams sharing models across Postgres/SQLite/Spanner or migrating to Spanner. The result is friction, boilerplate workarounds, and dialect-specific migration hacks.
---
## **Describe the solution you'd like**
Please add native support to automatically **translate `UNIQUE` constraints into unique indexes** for Spanner.
Concretely:
- When Alembic autogenerates migrations for Spanner, emit `op.create_index(..., unique=True)` instead of `op.create_unique_constraint(...)`.
- When running migrations containing `CreateUniqueConstraintOp` / `AddConstraintOp(UniqueConstraint)` / `DropConstraintOp(unique)`, transparently execute `CREATE UNIQUE INDEX` / `DROP INDEX`.
- Optionally, add a flag like `spanner_enforce_unique_via_index=True` to control this behavior.
This would let models using `unique=True` or `UniqueConstraint` work on Spanner without any schema or migration rewrites.
---
## **Describe alternatives you've considered**
1. **Custom Alembic hooks / dialect patches:**
- Developers can use `process_revision_directives` or custom Alembic `renderers` to rewrite unique constraints into unique indexes manually.
- This works but requires significant boilerplate (that should be in this repository to be reused).
2. **Dialect-specific model changes:**
- Removing `unique=True` or duplicating with explicit `Index(..., unique=True)` definitions.
- This breaks cross-dialect compatibility and influences peoples models too much to be generic.
3. **Ignoring uniqueness enforcement on Spanner:**
- Skipping constraints entirely is unsafe, as uniqueness violations go undetected.
- This is probably the simplest and better than just exploding people's migrations so long as a warning is emitted.
---
## **Additional context**
Spanner supports **unique indexes** but not **unique constraints**.
Supporting this translation natively would make cross-dialect ORM models compatible without user intervention and would align Spanner’s dialect with the expectations of the broader SQLAlchemy ecosystem.
### Minimal (Vibed from my code pasted in googleapis/python-spanner-sqlalchemy#228) Proof of Concept
Below is a minimal example using Alembic’s renderer dispatch to transparently rewrite unique constraints into unique indexes **only for Spanner**.
```python
# spanner_renderers.py
from alembic.autogenerate import renderers
from alembic.autogenerate.api import AutogenContext
from alembic.operations import ops
from sqlalchemy.sql.schema import UniqueConstraint
def _mk_idx_name(table: str, cols, name: str | None) -> str:
return name or f"uq_{table}_{'_'.join(cols)}"
def _render_create_idx(table: str, cols, name: str | None, schema: str | None) -> str:
idx_name = _mk_idx_name(table, cols, name)
parts = [repr(idx_name), repr(table), repr(cols), "unique=True"]
if schema:
parts.append(f"schema={schema!r}")
return f"op.create_index({', '.join(parts)})"
def _render_drop_idx(table: str, name: str, schema: str | None) -> str:
parts = [repr(name), f"table_name={table!r}"]
if schema:
parts.append(f"schema={schema!r}")
return f"op.drop_index({', '.join(parts)})"
# CreateUniqueConstraintOp → create unique index (Spanner)
@renderers.dispatch_for(ops.CreateUniqueConstraintOp, "spanner")
def _render_create_uc_spanner(autogen_context: AutogenContext, op: ops.CreateUniqueConstraintOp) -> str:
return _render_create_idx(op.table_name, list(op.columns), op.constraint_name, op.schema)
# AddConstraintOp(UniqueConstraint(...)) → create unique index (Spanner)
@renderers.dispatch_for(ops.AddConstraintOp, "spanner")
def _render_add_uc_spanner(autogen_context: AutogenContext, op: ops.AddConstraintOp) -> str:
cons = op.constraint
if isinstance(cons, UniqueConstraint):
cols = [c.name for c in cons.columns]
return _render_create_idx(cons.table.name, cols, cons.name, cons.table.schema)
raise NotImplementedError
# DropConstraintOp(unique) → drop index (Spanner)
@renderers.dispatch_for(ops.DropConstraintOp, "spanner")
def _render_drop_uc_spanner(autogen_context: AutogenContext, op: ops.DropConstraintOp) -> str:
if op.constraint_type == "unique":
return _render_drop_idx(op.table_name, op.constraint_name, op.schema)
raise NotImplementedError
コントリビューションガイド
評価
この issue はまだ評価されていません。