aachurin/peewee_migrations

Constraint not found when adding on_delete

Closed this issue · 18 comments

Hello,
I have a Postgres database created with peewee and peewee migrations (actually two of them, one staging and one production).

I recently added a new migration, I needed to add on_delete=CASCADE to some many to many and one to many relations.
Now, that worked super fine on staging, but then on production, when running pem migrate, it complains that it cannot find the foreign key.

This is the output of the pem show command

  SQL> ALTER TABLE "group_entity_through" DROP CONSTRAINT "<FOREIGN KEY>" []
  SQL> DROP INDEX "groupentitythrough_entity_id" []
  SQL> ALTER TABLE "group_entity_through" DROP CONSTRAINT "<FOREIGN KEY>" []
  SQL> CREATE INDEX "groupentitythrough_group_id" ON "group_entity_through" ("group_id") []
  SQL> ALTER TABLE "group_entity_through" ADD FOREIGN KEY ("group_id") REFERENCES "group" ("id") ON DELETE CASCADE []
  SQL> CREATE INDEX "groupentitythrough_entity_id" ON "group_entity_through" ("entity_id") []
  SQL> ALTER TABLE "group_entity_through" ADD FOREIGN KEY ("entity_id") REFERENCES "entity" ("entity_id") ON DELETE CASCADE []
  PY>  set_done('0012_migration_202201181039')

and this is the trace after executing the migration

Traceback (most recent call last):
  File "/home/user/project/python3.7/site-packages/peewee.py", line 3099, in execute_sql
    cursor.execute(sql, params or ())
psycopg2.errors.UndefinedObject: constraint "<FOREIGN KEY>" of relation "group_entity_through" does not exist


During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/user/project/python3.7/site-packages/peewee_migrations/migrator.py", line 759, in run
    op.run()
  File "/home/user/project/python3.7/site-packages/peewee_migrations/migrator.py", line 1114, in run
    self.database.execute(self.obj, scope=peewee.SCOPE_VALUES)
  File "/home/user/project/python3.7/site-packages/peewee.py", line 3112, in execute
    return self.execute_sql(sql, params, commit=commit)
  File "/home/user/project/python3.7/site-packages/peewee.py", line 3106, in execute_sql
    self.commit()
  File "/home/user/project/python3.7/site-packages/peewee.py", line 2873, in __exit__
    reraise(new_type, new_type(exc_value, *exc_args), traceback)
  File "/home/user/project/python3.7/site-packages/peewee.py", line 183, in reraise
    raise value.with_traceback(tb)
  File "/home/user/project/python3.7/site-packages/peewee.py", line 3099, in execute_sql
    cursor.execute(sql, params or ())
peewee.ProgrammingError: constraint "<FOREIGN KEY>" of relation "group_entity_through" does not exist


During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/user/project/python3.7/site-packages/peewee.py", line 3099, in execute_sql
    cursor.execute(sql, params or ())
psycopg2.errors.InFailedSqlTransaction: current transaction is aborted, commands ignored until end of transaction block


During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "//home/user/project/venv/bin/pem", line 8, in <module>
    sys.exit(run())
  File "/home/user/project/python3.7/site-packages/peewee_migrations/cli.py", line 347, in run
    cli()
  File "/home/user/project/python3.7/site-packages/click/core.py", line 1128, in __call__
    return self.main(*args, **kwargs)
  File "/home/user/project/python3.7/site-packages/click/core.py", line 1053, in main
    rv = self.invoke(ctx)
  File "/home/user/project/python3.7/site-packages/click/core.py", line 1659, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/user/project/python3.7/site-packages/click/core.py", line 1395, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/user/project/python3.7/site-packages/click/core.py", line 754, in invoke
    return __callback(*args, **kwargs)
  File "/home/user/project/python3.7/site-packages/click/decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/home/user/project/python3.7/site-packages/peewee_migrations/cli.py", line 339, in migrate
    step.run(fake=fake)
  File "/home/user/project/python3.7/site-packages/peewee_migrations/migrator.py", line 761, in run
    raise Exception(str(e), op.description)
  File "/home/user/project/python3.7/site-packages/peewee_migrations/migrator.py", line 1121, in description
    obj = ctx.sql(obj)
  File "/home/user/project/python3.7/site-packages/peewee.py", line 606, in sql
    return obj.__sql__(self)
  File "/home/user/project/python3.7/site-packages/peewee_migrations/migrator.py", line 1317, in __sql__
    value = value()
  File "/home/user/project/python3.7/site-packages/peewee_migrations/migrator.py", line 1393, in get_name
    cursor = self.database.execute_sql(sql, params)
  File "/home/user/project/python3.7/site-packages/peewee.py", line 3106, in execute_sql
    self.commit()
  File "/home/user/project/python3.7/site-packages/peewee.py", line 2873, in __exit__
    reraise(new_type, new_type(exc_value, *exc_args), traceback)
  File "/home/user/project/python3.7/site-packages/peewee.py", line 183, in reraise
    raise value.with_traceback(tb)
  File "/home/user/project/python3.7/site-packages/peewee.py", line 3099, in execute_sql
    cursor.execute(sql, params or ())
peewee.InternalError: current transaction is aborted, commands ignored until end of transaction block

Thank you in advance

Hi! Need the code to reproduce.

Well, first of all thanks for the blazing fast reply. Here is the piece of code for that

import logging
import datetime
from peewee import (Model, CharField, BooleanField,
                    DateTimeField, TimestampField,
                    DoubleField, IntegerField,
                    ForeignKeyField, ManyToManyField)
from common.database import get_db


class BaseModel(Model):
    created = DateTimeField(default=datetime.datetime.now)
    modified = DateTimeField()

    def save(self, *args, **kwargs):
        self.modified = datetime.datetime.now()
        return super(BaseModel, self).save(*args, **kwargs)

    class Meta:
        database = get_db()


class OwnedBaseModel(BaseModel):
    user_id = CharField(null=False, index=True, max_length=36)
    email = CharField(null=True, default='')


class Entity(OwnedBaseModel):
    entity_id = CharField(null=False, primary_key=True)
    name = CharField(null=False)

class Group(OwnedBaseModel):
    name = CharField(null=False)
    endpoint_id = CharField(unique=True, max_length=36, null=True)
    ### on_delete was added here!
    entities = ManyToManyField(Entity, backref='groups', on_delete='CASCADE') 
    class Meta:
        indexes = (
            (('name', 'user_id', ), True),
        )

Also, I'd like to stress once again that this worked super smooth on staging, but not on prod. I did a sql dump schema only of both, and everything seems ok

It's work for me.

pem show:

[ ] 0002_migration_202201191856:
  [*] SQL> ALTER TABLE "group_entity_through" DROP CONSTRAINT "group_entity_through_group_id_fkey" []
  [*] SQL> DROP INDEX "groupentitythrough_group_id" []
  [*] SQL> ALTER TABLE "group_entity_through" DROP CONSTRAINT "group_entity_through_entity_id_fkey" []
  [*] SQL> DROP INDEX "groupentitythrough_entity_id" []
  [*] SQL> CREATE INDEX "groupentitythrough_group_id" ON "group_entity_through" ("group_id") []
  [*] SQL> ALTER TABLE "group_entity_through" ADD FOREIGN KEY ("group_id") REFERENCES "group" ("id") ON DELETE CASCADE []
  [*] SQL> CREATE INDEX "groupentitythrough_entity_id" ON "group_entity_through" ("entity_id") []
  [*] SQL> ALTER TABLE "group_entity_through" ADD FOREIGN KEY ("entity_id") REFERENCES "entity" ("entity_id") ON DELETE CASCADE []
  [*] PY>  set_done('0002_migration_202201191856')

It looks like there is a divergent between migrations and bases in production and stage.
Receiving <FOREIGN KEY> for the closest next migration in watch means that FK does not exist (was not created, was created with different name, or was renamed)

Yeah no I think I've found the issue, on prod we are using a different schema than public.
This is weird tho, never have I had to specify it before.

Is there a way to set it in the connecting config?

For psycopg2 is options keyword.

conn = psycopg2.connect(..., options=f'-c search_path={schema}') 

Should be the same for peewee database instance.

I don't know if it willl work. But to my mind it's a problem if you have different schemas in stage and production.

Migrator should know the schema name to find FK.

Yes that's exactly what I meant in my last comment, different schemas :) I have now the problem that the schema is embedded into the migration file as a parameter of the meta class, but I will find a workaround.

Will post an update when done but that should do the trick.

Thanks!

Just a final note, migrations so far run without this being an issue

Setting the schema and re-creating the scenario now give the expected migration file Metas

    class Meta:
        table_name = "device_entity_through"
        schema = "mycustomeschema"

But when I run it with pem show, the params in here

def _drop_foreign_key_constraint(self, model, field):
        params = (model._meta.table_name,
                  model._meta.schema or 'public',
                  field.rel_model._meta.table_name,
                  field.rel_model._meta.schema or 'public',
                  field.column_name,
                  field.rel_field.column_name)

        def get_name():
            sql = (
                "SELECT tc.constraint_name "
                "FROM information_schema.table_constraints AS tc "
                "JOIN information_schema.key_column_usage AS kcu "
                "ON (tc.constraint_name = kcu.constraint_name AND "
                "tc.constraint_schema = kcu.constraint_schema) "
                "JOIN information_schema.constraint_column_usage AS ccu "
                "ON (ccu.constraint_name = tc.constraint_name AND "
                "ccu.constraint_schema = tc.constraint_schema) "
                "WHERE "
                "tc.constraint_type = 'FOREIGN KEY' AND "
                "tc.table_name = %s AND "
                "tc.table_schema = %s AND "
                "ccu.table_name = %s AND "
                "ccu.table_schema = %s AND "
                "kcu.column_name = %s AND "
                "ccu.column_name = %s"
            )
            import pudb
            pudb.set_trace()
            cursor = self.database.execute_sql(sql, params)
            result = cursor.fetchall()
            return peewee.Entity(result[0][0] if result else '<FOREIGN KEY>')

        return (LazyQuery().literal('ALTER TABLE ')
                           .sql(model._meta.table)
                           .literal(' DROP CONSTRAINT ')
                           .sql(get_name))

And yeah, I know it's a super old version :) will do an upgrade soon

Just added --explicit_schema option. Try it from repo.
pem show --explicit_schema myschema
pem migrate --explicit_schema myschema

If it works I will make release to pypi.

Any way, looks like it works. Made a release.

So, if you have another default schema (search_path) in connection string you should set explicit_schema argument for show and migrate commands. In this case Migrator will know where to look for indexes.

Wow, that was fast! I can't test untill tomorrow unfortunately but I'm fairly sure that will work.

Real MVP!! Thank you so much mate, will post an update tomorrow :)

And another great option. Auto-detect default schema from search_path.
If you don't set explicit_schema, SHOW search_path will be executed, and selected as a default schema ($user schema is not supported any way and will be ignored, but it's not your case)

Hey, I've been testing around, I am not sure whether you should set the explicit schema also on the rel_schema here https://github.com/aachurin/peewee_migrations/blob/master/peewee_migrations/migrator.py#L1593

I have tried locally and made it possible to identify the correct schema, nevertheless, I have the feeling that this is just over complicating the matter for everybody - my setup is particular with the different default schema.

In my opinion this should be handled by the connection manager, not the migration (but I also might be wrong). I will maybe submit a PR or fork if I have time to dig more and think of a more elegant solution.

Hey, I've been testing around, I am not sure whether you should set the explicit schema also on the rel_schema here https://github.com/aachurin/peewee_migrations/blob/master/peewee_migrations/migrator.py#L1593

Fixed!

I have tried locally and made it possible to identify the correct schema, nevertheless, I have the feeling that this is just over complicating the matter for everybody - my setup is particular with the different default schema.
In my opinion this should be handled by the connection manager, not the migration (but I also might be wrong). I will maybe submit a PR or fork if I have time to dig more and think of a more elegant solution.

Already done it here:

schemas = [x.strip() for x in self._database.execute_sql("show search_path").fetchone()[0].split(",")]

Yeah saw that about the default schema, problem is,in the next few lines you pop "$user" but it should actually be replace with the current user. Because that's exactly my case, and probably will be others too.

Example: my user for connection is nick, then the schema is also nick (replacement for that user var over there).

But anyway, using the explicit schema did the trick for now :) again, thank you!

Try it now:

for schema in self._database.execute_sql("show search_path").fetchone()[0].split(","):

Yeah that's a good approach and works for me too! (Although I did fix it using the explicit schema for now, as I said).

Thank you!