olirice / alembic_utils

An alembic/sqlalchemy extension for migrating sql views, functions, triggers, and policies
https://olirice.github.io/alembic_utils
MIT License
211 stars 43 forks source link

Must be owner of materialized view mat_view_name #109

Closed captainvera closed 1 year ago

captainvera commented 1 year ago

Hey there, First of all, thanks for this great package!

I am managing a simple DB with 1 table and a materialized view on that table.

It is a managed Pgsql DB in aws and I have access to it through StrongDM. In our production environment we have a user whose credentials get shared directly with the kubernetes pods that need access to this DB.

My usual workflow is creating the alembic migration locally with alembic revision -m "..." --autogenerate", commit this to the remote repo and on deployment a k8s job runs and does alembic upgrade head.

This worked great with everything related to the table and for creating the materialized view.

However, after the creation of the materialized view, every time I try to make a new migration (unrelated to the materialized view) I get the following error:

Traceback (most recent call last):
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1900, in _execute_context
    self.dialect.do_execute(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 736, in do_execute
    cursor.execute(statement, parameters)
psycopg2.errors.InsufficientPrivilege: must be owner of materialized view qe_cua_materialized_view_v2

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic_utils/simulate.py", line 47, in simulate_entity
    sess.execute(entity.to_sql_statement_drop(cascade=True))
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/orm/session.py", line 1712, in execute
    result = conn._execute_20(statement, params or {}, execution_options)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1705, in _execute_20
    return meth(self, args_10style, kwargs_10style, execution_options)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/sql/elements.py", line 333, in _execute_on_connection
    return connection._execute_clauseelement(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1572, in _execute_clauseelement
    ret = self._execute_context(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1943, in _execute_context
    self._handle_dbapi_exception(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 2124, in _handle_dbapi_exception
    util.raise_(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/util/compat.py", line 208, in raise_
    raise exception
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1900, in _execute_context
    self.dialect.do_execute(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 736, in do_execute
    cursor.execute(statement, parameters)
sqlalchemy.exc.ProgrammingError: (psycopg2.errors.InsufficientPrivilege) must be owner of materialized view qe_cua_materialized_view_v2

[SQL: DROP MATERIALIZED VIEW "public"."qe_cua_materialized_view_v2" cascade]
(Background on this error at: https://sqlalche.me/e/14/f405)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1900, in _execute_context
    self.dialect.do_execute(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 736, in do_execute
    cursor.execute(statement, parameters)
psycopg2.errors.DuplicateTable: relation "qe_cua_materialized_view_v2" already exists

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/miguelvera/miniconda3/envs/qms/bin/alembic", line 8, in <module>
    sys.exit(main())
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/config.py", line 617, in main
    CommandLine(prog=prog).main(argv=argv)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/config.py", line 611, in main
    self.run_cmd(cfg, options)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/config.py", line 588, in run_cmd
    fn(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/command.py", line 236, in revision
    script_directory.run_env()
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/script/base.py", line 582, in run_env
    util.load_python_file(self.dir, "env.py")
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/util/pyfiles.py", line 94, in load_python_file
    module = load_module_py(module_id, path)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/util/pyfiles.py", line 110, in load_module_py
    spec.loader.exec_module(module)  # type: ignore
  File "<frozen importlib._bootstrap_external>", line 850, in exec_module
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File "migrations/env.py", line 89, in <module>
    run_migrations_online()
  File "migrations/env.py", line 83, in run_migrations_online
    context.run_migrations()
  File "<string>", line 8, in run_migrations
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/runtime/environment.py", line 928, in run_migrations
    self.get_context().run_migrations(**kw)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/runtime/migration.py", line 615, in run_migrations
    for step in self._migrations_fn(heads, self):
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/command.py", line 212, in retrieve_migrations
    revision_context.run_autogenerate(rev, context)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/autogenerate/api.py", line 562, in run_autogenerate
    self._run_environment(rev, migration_context, True)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/autogenerate/api.py", line 609, in _run_environment
    compare._populate_migration_script(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/autogenerate/compare.py", line 59, in _populate_migration_script
    _produce_net_changes(autogen_context, upgrade_ops)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/autogenerate/compare.py", line 93, in _produce_net_changes
    comparators.dispatch("schema", autogen_context.dialect.name)(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic/util/langhelpers.py", line 269, in go
    fn(*arg, **kw)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic_utils/replaceable_entity.py", line 328, in compare_registered_entities
    maybe_op = entity.get_required_migration_op(sess, dependencies=has_create_or_update_op)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic_utils/replaceable_entity.py", line 163, in get_required_migration_op
    db_def = self.get_database_definition(sess, dependencies=dependencies)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic_utils/replaceable_entity.py", line 102, in get_database_definition
    with simulate_entity(sess, self, dependencies) as sess:
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/contextlib.py", line 119, in __enter__
    return next(self.gen)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/alembic_utils/simulate.py", line 62, in simulate_entity
    sess.execute(entity.to_sql_statement_create())
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/orm/session.py", line 1712, in execute
    result = conn._execute_20(statement, params or {}, execution_options)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1705, in _execute_20
    return meth(self, args_10style, kwargs_10style, execution_options)
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/sql/elements.py", line 333, in _execute_on_connection
    return connection._execute_clauseelement(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1572, in _execute_clauseelement
    ret = self._execute_context(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1943, in _execute_context
    self._handle_dbapi_exception(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 2124, in _handle_dbapi_exception
    util.raise_(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/util/compat.py", line 208, in raise_
    raise exception
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1900, in _execute_context
    self.dialect.do_execute(
  File "/home/miguelvera/miniconda3/envs/qms/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 736, in do_execute
    cursor.execute(statement, parameters)
sqlalchemy.exc.ProgrammingError: (psycopg2.errors.DuplicateTable) relation "qe_cua_materialized_view_v2" already exists

It seems that I need to be the owner of the materialized view? But I am making no changes on it.

Two other interesting things also happened:

  1. I could still remove the mat view from the call to register_entities in alembic's env.py and it will allow me to run the migration without dropping the view.
  2. If I remove it from register_entities and add a new one (instead of an empty list) it will actually drop the old one and create the new one.

The current workaround I am using to continue doing migrations is 1. but it is a weird hack and I am sure this is not intended behaviour. I also see no reason that alembic_utils would need me to be the owner of the mat view when doing no changes to it and running an unrelated migration, especially when I can do it fine for all other entities.

Thanks in advance, Miguel

olirice commented 1 year ago

If I follow, the error is occurring while trying to autogenerated a new migration, not during deployment, correct?


Materialized views allows arbitrary SQL in their definitions, which is different from the entities alembic manages. To account for that, the diffing during autogenerate uses a separate workflow. When you create a materialized view in postgres it stores the underlying query. That query is parsed and reformatted so your local text blob in your python project no longer matches the text that is stored in postgres. That makes it difficult to determine if the definition has changed during --autogenerate

The way alembic_utils checks to see if the definition of a materialized view has changed is to:

  1. look at the mat views current definition in the database and then, in a transaction:
  2. drop the mat view
  3. recreate the mat view from the local definition
  4. check if the definition in the db is different than what we started with
  5. roll back the transaction

The error your seeing is occurring at step 2 while alembic_utils is figuring out which (if any) of your local database entities have changed.


A solution to get you unblocked locally would be to manually execute

alter materialized view <mat view schema>.<mat view name> owner to <local migration role name>

to re-align the materialized view's owner to the role that produces the autogenerated migrations, but that shouldn't be necessary


Are you aware of any reason why the role name that is locally producing the migrations would differ from the role that applied the migrations to that local instance?

For example, spinning up your local development database from a dump of production where the role names are different would cause this

captainvera commented 1 year ago

Thank you for the explanation!

From your questions at the end I understand what I am doing wrong. You see, I am not connecting to a local development database, but running the alembic revision command while connecting directly to a hosted staging DB.

Because I was not aware of any sort of transaction actually being made I believed it to be a no-impact process and as such not needing a local dev DB (not best practices but hey :) ).

In the end what is happening is exactly what you described, the role that applies the migrations in staging is setup similarly to the one i mentioned on the original issue with a k8s job, so the role that applies the migration (remotely) is indeed different than the one that creates the migrations locally.

Guess I'll have to setup some best practices! Thanks again!