ovh/celery-director

New column can't be added with the current migrations

ncrocfer opened this issue · 0 comments

A bug has been found by @henri42 in the #162 PR: it's currently impossible to add new column.

For instance:

$ cat director/migrations/versions/9d563aaa548b_add_workflows_description.py
...
def upgrade():
    op.add_column(
        "workflows",
        sa.Column("description", sa.String(255), nullable=True),
    )


def downgrade():
    op.drop_column("workflows", "description")

Applying this migration will raise a sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: workflows.description error.

Full traceback
$ flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 30d6f6636351, Initial migration
INFO  [alembic.runtime.migration] Running upgrade 30d6f6636351 -> 05cf96d6fcae, Add task result
INFO  [alembic.runtime.migration] Running upgrade 05cf96d6fcae -> 3f8466b16023, Add users table
INFO  [alembic.runtime.migration] Running upgrade 3f8466b16023 -> 063ff371f2da, Add index on workflow_id in task table
INFO  [alembic.runtime.migration] Running upgrade 063ff371f2da -> 2ac615d6850b, Force varchar 255
Traceback (most recent call last):
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1245, in _execute_context
  self.dialect.do_execute(
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/sqlalchemy/engine/default.py", line 581, in do_execute
  cursor.execute(statement, parameters)
sqlite3.OperationalError: no such column: workflows.description

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

Traceback (most recent call last):
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/bin/flask", line 8, in <module>
  sys.exit(main())
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/flask/cli.py", line 985, in main
  cli.main()
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/flask/cli.py", line 579, in main
  return super().main(*args, **kwargs)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/click/core.py", line 1055, in main
  rv = self.invoke(ctx)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
  return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/click/core.py", line 1657, in invoke
  return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/click/core.py", line 1404, in invoke
  return ctx.invoke(self.callback, **ctx.params)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/click/core.py", line 760, in invoke
  return __callback(*args, **kwargs)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/click/decorators.py", line 26, in new_func
  return f(get_current_context(), *args, **kwargs)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/flask/cli.py", line 427, in decorator
  return __ctx.invoke(f, *args, **kwargs)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/click/core.py", line 760, in invoke
  return __callback(*args, **kwargs)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/flask_migrate/cli.py", line 149, in upgrade
  _upgrade(directory, revision, sql, tag, x_arg)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/flask_migrate/__init__.py", line 98, in wrapped
  f(*args, **kwargs)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/flask_migrate/__init__.py", line 185, in upgrade
  command.upgrade(config, revision, sql=sql, tag=tag)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/alembic/command.py", line 298, in upgrade
  script.run_env()
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/alembic/script/base.py", line 489, in run_env
  util.load_python_file(self.dir, "env.py")
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/alembic/util/pyfiles.py", line 98, in load_python_file
  module = load_module_py(module_id, path)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/alembic/util/compat.py", line 173, in load_module_py
  spec.loader.exec_module(module)
File "<frozen importlib._bootstrap_external>", line 783, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "/Users/ncrocfer/Dev/deploy/celery-director/director/migrations/env.py", line 96, in <module>
  run_migrations_online()
File "/Users/ncrocfer/Dev/deploy/celery-director/director/migrations/env.py", line 90, in run_migrations_online
  context.run_migrations()
File "<string>", line 8, in run_migrations
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/alembic/runtime/environment.py", line 846, in run_migrations
  self.get_context().run_migrations(**kw)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/alembic/runtime/migration.py", line 518, in run_migrations
  step.migration_fn(**kw)
File "/Users/ncrocfer/Dev/deploy/celery-director/director/migrations/versions/2ac615d6850b_force_varchar_255.py", line 57, in upgrade
  batch_op.alter_column(
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.8/lib/python3.8/contextlib.py", line 120, in __exit__
  next(self.gen)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/alembic/operations/base.py", line 325, in batch_alter_table
  impl.flush()
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/alembic/operations/batch.py", line 108, in flush
  batch_impl._create(self.impl)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/alembic/operations/batch.py", line 290, in _create
  op_impl._exec(
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/alembic/ddl/impl.py", line 134, in _exec
  return conn.execute(construct, *multiparams, **params)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 982, in execute
  return meth(self, multiparams, params)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/sqlalchemy/sql/elements.py", line 287, in _execute_on_connection
  return connection._execute_clauseelement(self, multiparams, params)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1095, in _execute_clauseelement
  ret = self._execute_context(
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1249, in _execute_context
  self._handle_dbapi_exception(
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1476, in _handle_dbapi_exception
  util.raise_from_cause(sqlalchemy_exception, exc_info)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/sqlalchemy/util/compat.py", line 398, in raise_from_cause
  reraise(type(exception), exception, tb=exc_tb, cause=cause)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/sqlalchemy/util/compat.py", line 152, in reraise
  raise value.with_traceback(tb)
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1245, in _execute_context
  self.dialect.do_execute(
File "/Users/ncrocfer/Dev/deploy/celery-director/venv/lib/python3.8/site-packages/sqlalchemy/engine/default.py", line 581, in do_execute
  cursor.execute(statement, parameters)
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: workflows.description
[SQL: INSERT INTO _alembic_tmp_workflows (id, created_at, updated_at, name, project, status, payload, periodic, description) SELECT workflows.id, workflows.created_at, workflows.updated_at, workflows.name, workflows.project, workflows.status, workflows.payload, workflows.periodic, workflows.description
FROM workflows]
(Background on this error at: http://sqlalche.me/e/e3q8)

As mentionned by @henri42 in his PR we added a special syntax in the 2ac615d6850b_force_varchar_255.py migration.

This syntax was dedicated to the sqlite engine which doesn't support some manipulations. As mentioned in this article from the Flask-Migrate creator:

Changes that you make to the fields in your model or to the constraints associated with them will end up as an ALTER TABLE statement sent to the database. If you are using MySQL, Postgres or most other database servers besides SQLite, this isn't a problem. With SQLite, however, the ALTER TABLE command only supports adding or renaming columns. Any other change to columns or constraints is going to be rejected with an error.

So we use the following syntax to update the length of varchars:

with op.batch_alter_table("workflows") as batch_op:
    batch_op.alter_column(
        "name",
        existing_type=sa.VARCHAR(),
        type_=sa.String(length=255),
        existing_nullable=False,
    )

But a new bug had been raised for the sqlite engine: it was impossible to use the flask db upgrade --sql command (used to display the SQL queries). So we created a new PR #85 to use the offline mode of Alembic instead the online one (more information here).

So the new copy_from argument:

with op.batch_alter_table("workflows", copy_from=some_table) as batch_op:
    batch_op.alter_column(...)

But as said in this doc:

The above use pattern is pretty tedious and quite far off from Alembic’s preferred style of working; however, if one needs to do SQLite-compatible “move and copy” migrations and need them to generate flat SQL files in “offline” mode, there’s not much alternative.

So now we can't add new columns... 😒

I think we'll simply remove this copy_from argument and we'll reuse the online mode. The consequence will be the impossibility to generate flat SQL files using the sqlite engine. But for me this problem is not a real one:

  • sqlite is used when Celery Director users want to develop in local, so generate SQL files is not a real need here
  • when the project goes into prod, a more robust engine has to be used (like PostgreSQL or MySQL). In this case SQL files generation will be possible and users will be able to send them to their Database Administrator 👌

@ovh/gistools members, what do you think about that?