Title: Support automatic translation of unique=True / UniqueConstraint to unique indexes in migrations · Issue #765 · googleapis/python-spanner-sqlalchemy · GitHub
Open Graph Title: Support automatic translation of unique=True / UniqueConstraint to unique indexes in migrations · Issue #765 · googleapis/python-spanner-sqlalchemy
X Title: Support automatic translation of unique=True / UniqueConstraint to unique indexes in migrations · Issue #765 · googleapis/python-spanner-sqlalchemy
Description: Is your feature request related to a problem? Please describe. Yes. And relates to #228 The Cloud Spanner SQLAlchemy dialect (python-spanner-sqlalchemy) does not support table-level UNIQUE constraints, while the idiomatic SQLAlchemy/SQLM...
Open Graph Description: Is your feature request related to a problem? Please describe. Yes. And relates to #228 The Cloud Spanner SQLAlchemy dialect (python-spanner-sqlalchemy) does not support table-level UNIQUE constrai...
X Description: Is your feature request related to a problem? Please describe. Yes. And relates to #228 The Cloud Spanner SQLAlchemy dialect (python-spanner-sqlalchemy) does not support table-level UNIQUE constrai...
Opengraph URL: https://github.com/googleapis/python-spanner-sqlalchemy/issues/765
X: @github
Domain: patch-diff.githubusercontent.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Support automatic translation of unique=True / UniqueConstraint to unique indexes in migrations","articleBody":"## **Is your feature request related to a problem? Please describe.**\n\nYes. And relates to #228 \n\nThe 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__`.\n\nWhen 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. \n\nThis 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.\n\n---\n\n## **Describe the solution you'd like**\n\nPlease add native support to automatically **translate `UNIQUE` constraints into unique indexes** for Spanner. \n\nConcretely:\n\n- When Alembic autogenerates migrations for Spanner, emit `op.create_index(..., unique=True)` instead of `op.create_unique_constraint(...)`.\n- When running migrations containing `CreateUniqueConstraintOp` / `AddConstraintOp(UniqueConstraint)` / `DropConstraintOp(unique)`, transparently execute `CREATE UNIQUE INDEX` / `DROP INDEX`.\n- Optionally, add a flag like `spanner_enforce_unique_via_index=True` to control this behavior.\n\nThis would let models using `unique=True` or `UniqueConstraint` work on Spanner without any schema or migration rewrites.\n\n---\n\n## **Describe alternatives you've considered**\n\n1. **Custom Alembic hooks / dialect patches:** \n - Developers can use `process_revision_directives` or custom Alembic `renderers` to rewrite unique constraints into unique indexes manually. \n - This works but requires significant boilerplate (that should be in this repository to be reused).\n\n2. **Dialect-specific model changes:** \n - Removing `unique=True` or duplicating with explicit `Index(..., unique=True)` definitions. \n - This breaks cross-dialect compatibility and influences peoples models too much to be generic.\n\n3. **Ignoring uniqueness enforcement on Spanner:** \n - Skipping constraints entirely is unsafe, as uniqueness violations go undetected.\n - This is probably the simplest and better than just exploding people's migrations so long as a warning is emitted.\n\n---\n\n## **Additional context**\n\nSpanner supports **unique indexes** but not **unique constraints**. \nSupporting 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.\n\n### Minimal (Vibed from my code pasted in #228) Proof of Concept\n\nBelow is a minimal example using Alembic’s renderer dispatch to transparently rewrite unique constraints into unique indexes **only for Spanner**.\n\n```python\n# spanner_renderers.py\nfrom alembic.autogenerate import renderers\nfrom alembic.autogenerate.api import AutogenContext\nfrom alembic.operations import ops\nfrom sqlalchemy.sql.schema import UniqueConstraint\n\ndef _mk_idx_name(table: str, cols, name: str | None) -\u003e str:\n return name or f\"uq_{table}_{'_'.join(cols)}\"\n\ndef _render_create_idx(table: str, cols, name: str | None, schema: str | None) -\u003e str:\n idx_name = _mk_idx_name(table, cols, name)\n parts = [repr(idx_name), repr(table), repr(cols), \"unique=True\"]\n if schema:\n parts.append(f\"schema={schema!r}\")\n return f\"op.create_index({', '.join(parts)})\"\n\ndef _render_drop_idx(table: str, name: str, schema: str | None) -\u003e str:\n parts = [repr(name), f\"table_name={table!r}\"]\n if schema:\n parts.append(f\"schema={schema!r}\")\n return f\"op.drop_index({', '.join(parts)})\"\n\n# CreateUniqueConstraintOp → create unique index (Spanner)\n@renderers.dispatch_for(ops.CreateUniqueConstraintOp, \"spanner\")\ndef _render_create_uc_spanner(autogen_context: AutogenContext, op: ops.CreateUniqueConstraintOp) -\u003e str:\n return _render_create_idx(op.table_name, list(op.columns), op.constraint_name, op.schema)\n\n# AddConstraintOp(UniqueConstraint(...)) → create unique index (Spanner)\n@renderers.dispatch_for(ops.AddConstraintOp, \"spanner\")\ndef _render_add_uc_spanner(autogen_context: AutogenContext, op: ops.AddConstraintOp) -\u003e str:\n cons = op.constraint\n if isinstance(cons, UniqueConstraint):\n cols = [c.name for c in cons.columns]\n return _render_create_idx(cons.table.name, cols, cons.name, cons.table.schema)\n raise NotImplementedError\n\n# DropConstraintOp(unique) → drop index (Spanner)\n@renderers.dispatch_for(ops.DropConstraintOp, \"spanner\")\ndef _render_drop_uc_spanner(autogen_context: AutogenContext, op: ops.DropConstraintOp) -\u003e str:\n if op.constraint_type == \"unique\":\n return _render_drop_idx(op.table_name, op.constraint_name, op.schema)\n raise NotImplementedError","author":{"url":"https://github.com/MattOates","@type":"Person","name":"MattOates"},"datePublished":"2025-10-06T16:46:16.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":1},"url":"https://github.com/765/python-spanner-sqlalchemy/issues/765"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:e69c0537-32d8-a057-0ea0-c40aed576d51 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | C3FC:162063:5A9100E:7B56EEE:6978415E |
| html-safe-nonce | 0113bf791bb95358eac5ecea353e6ffbd82566c54f84ff6efe1f84d562cd3d7a |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJDM0ZDOjE2MjA2Mzo1QTkxMDBFOjdCNTZFRUU6Njk3ODQxNUUiLCJ2aXNpdG9yX2lkIjoiOTE4MTQyOTI5MDY4MjA0MDY3MCIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 504786385492cd6c09db045a10c695710c6cb57f21416a44feead3440e5a564e |
| hovercard-subject-tag | issue:3488169732 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/googleapis/python-spanner-sqlalchemy/765/issue_layout |
| twitter:image | https://opengraph.githubassets.com/22a2ff0cbffaa9d2a5b603bf254f40039e31760dbc29987ab8252bd92160cb46/googleapis/python-spanner-sqlalchemy/issues/765 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/22a2ff0cbffaa9d2a5b603bf254f40039e31760dbc29987ab8252bd92160cb46/googleapis/python-spanner-sqlalchemy/issues/765 |
| og:image:alt | Is your feature request related to a problem? Please describe. Yes. And relates to #228 The Cloud Spanner SQLAlchemy dialect (python-spanner-sqlalchemy) does not support table-level UNIQUE constrai... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | MattOates |
| hostname | github.com |
| expected-hostname | github.com |
| None | 2981c597c945c1d90ac6fa355ce7929b2f413dfe7872ca5c435ee53a24a1de50 |
| turbo-cache-control | no-preview |
| go-import | github.com/googleapis/python-spanner-sqlalchemy git https://github.com/googleapis/python-spanner-sqlalchemy.git |
| octolytics-dimension-user_id | 16785467 |
| octolytics-dimension-user_login | googleapis |
| octolytics-dimension-repository_id | 335511641 |
| octolytics-dimension-repository_nwo | googleapis/python-spanner-sqlalchemy |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 335511641 |
| octolytics-dimension-repository_network_root_nwo | googleapis/python-spanner-sqlalchemy |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 520b65a872113b919c1bbdb03834a50af15859fd |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width