From 858abd31cec0b17533c13abf7ea45758df282d44 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 19 Aug 2024 11:09:41 -0400 Subject: [PATCH] add mysql/mariadb to multi-tenant recipe we can use USE just as easily as search_path here, so add that. Change-Id: I0af8b7c15c9647c613ba6e0aae99173745df29af --- docs/build/cookbook.rst | 42 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/docs/build/cookbook.rst b/docs/build/cookbook.rst index fd84db0a..a9121730 100644 --- a/docs/build/cookbook.rst +++ b/docs/build/cookbook.rst @@ -776,8 +776,8 @@ recreated again within the downgrade for this migration:: .. _cookbook_postgresql_multi_tenancy: -Rudimental Schema-Level Multi Tenancy for PostgreSQL Databases -============================================================== +Rudimental Schema-Level Multi Tenancy for PostgreSQL, MySQL, Other Databases +============================================================================ **Multi tenancy** refers to an application that accommodates for many clients simultaneously. Within the scope of a database migrations tool, @@ -793,6 +793,11 @@ is to install tenants within **individual PostgreSQL schemas**. When using PostgreSQL's schemas, a special variable ``search_path`` is offered that is intended to assist with targeting of different schemas. +When using MySQL or MariaDB databases, a similar command is available at the +SQL level called the``use`` command. This command may be used in a similar +fashion as that of PostgreSQL's ``search_path`` variable to achieve a similar +effect. + .. note:: SQLAlchemy includes a system of directing a common set of ``Table`` metadata to many schemas called `schema_translate_map `_. Alembic at the time of this writing lacks adequate support for this feature. The recipe below @@ -801,7 +806,7 @@ intended to assist with targeting of different schemas. The recipe below can be altered for flexibility. The primary purpose of this recipe is to illustrate how to point the Alembic process towards one PostgreSQL -schema or another. +or MySQL/MariaDB schema or another. 1. The model metadata used as the target for autogenerate must not include any schema name for tables; the schema must be non-present or set to ``None``. @@ -841,12 +846,20 @@ schema or another. current_tenant = context.get_x_argument(as_dictionary=True).get("tenant") with connectable.connect() as connection: - # set search path on the connection, which ensures that - # PostgreSQL will emit all CREATE / ALTER / DROP statements - # in terms of this schema by default - connection.execute(text('set search_path to "%s"' % current_tenant)) - # in SQLAlchemy v2+ the search path change needs to be committed - connection.commit() + if connection.dialect.name == "postgresql": + # set search path on the connection, which ensures that + # PostgreSQL will emit all CREATE / ALTER / DROP statements + # in terms of this schema by default + + connection.execute(text('set search_path to "%s"' % current_tenant)) + # in SQLAlchemy v2+ the search path change needs to be committed + connection.commit() + elif connection.dialect.name in ("mysql", "mariadb"): + # set "USE" on the connection, which ensures that + # MySQL/MariaDB will emit all CREATE / ALTER / DROP statements + # in terms of this schema by default + + connection.execute(text('USE %s' % current_tenant)) # make use of non-supported SQLAlchemy attribute to ensure # the dialect reflects tables in terms of the current tenant name @@ -860,17 +873,18 @@ schema or another. with context.begin_transaction(): context.run_migrations() - The current tenant is set using the PostgreSQL ``search_path`` variable on - the connection. Note above we must employ a **non-supported SQLAlchemy - workaround** at the moment which is to hardcode the SQLAlchemy dialect's - default schema name to our target schema. + The current tenant is set using the PostgreSQL ``search_path`` variable, or + the MySQL/MariaDB ``USE`` statement, on the connection. Note above we must + employ a **non-supported SQLAlchemy workaround** at the moment which is to + hardcode the SQLAlchemy dialect's default schema name to our target schema. It is also important to note that the above changes **remain on the connection permanently unless reversed explicitly**. If the alembic application simply exits above, there is no issue. However if the application attempts to continue using the above connection for other purposes, it may be necessary to reset these variables back to the default, which for PostgreSQL is usually - the name "public" however may be different based on configuration. + the name "public" however may be different based on configuration, and + for MySQL/MariaDB is typically the "database" portion of the database URL. 4. Alembic operations will now proceed in terms of whichever schema we pass