Skip to content

Commit

Permalink
add mysql/mariadb to multi-tenant recipe
Browse files Browse the repository at this point in the history
we can use USE just as easily as search_path here, so add that.

Change-Id: I0af8b7c15c9647c613ba6e0aae99173745df29af
  • Loading branch information
zzzeek committed Aug 19, 2024
1 parent e210c24 commit 858abd3
Showing 1 changed file with 28 additions and 14 deletions.
42 changes: 28 additions & 14 deletions docs/build/cookbook.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 <https://docs.sqlalchemy.org/core/connections.html#translation-of-schema-names>`_. Alembic at the time
of this writing lacks adequate support for this feature. The recipe below
Expand All @@ -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``.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 858abd3

Please sign in to comment.