sqlalchemy.exc.CircularDependencyError: Can't sort tables for DROP; an unresolvable foreign key dependency exists between tables

Viewed 172

I'm stuck on the error in the title of this post. I have a table Question that has a double connection (one to one and one to many) with another table Answer. I can create the tables, but I can't drop them for some reason.

I tried to add a "primary join" argument as suggested in this post but to no avail.

Why is this code failing?

#Initialize database
db = SQLAlchemy(app)

class Question(db.Model):
    __tablename__ = "Question"
    id = Column(Integer, primary_key=True)
    question = Column(String(256),nullable=False)
    correct_answer_id = Column(Integer,ForeignKey('Answer.id'))

    correct_answer = relationship("Answer",foreign_keys=[correct_answer_id],uselist=False,primaryjoin="Question.correct_answer_id==Answer.id")
    answers = relationship("Answer", backref="Question", primaryjoin="Question.id==Answer.question_id")

class Answer(db.Model):
    __tablename__ = "Answer"
    id = Column(Integer, primary_key=True)
    question_id = Column(Integer, ForeignKey('Question.id'),nullable=False)
    answer = Column(String(256))

db.drop_all()

db.create_all()
2 Answers

The solution for your issue is in the part of the error message that you didn't include in the question:

sqlalchemy.exc.CircularDependencyError: Can't sort tables for DROP; an unresolvable foreign key dependency exists between tables: Answer, Question. Please ensure that the ForeignKey and ForeignKeyConstraint objects involved in the cycle have names so that they can be dropped using DROP CONSTRAINT.

That is, instead of

    correct_answer_id = Column(Integer, ForeignKey("Answer.id"))

use something like

    correct_answer_id = Column(
        Integer, ForeignKey("Answer.id", name="fk_question_correct_answer")
    )

(and similarly for the FK in the other table).

Edit:

If the table already exists and the database has assigned a name to the foreign key then you can use that name instead of making up your own. For example, SQL Server assigns names like this:

> select TABLE_NAME, CONSTRAINT_NAME from INFORMATION_SCHEMA.TABLE_CONSTRAINTS where CONSTRAINT_TYPE='FOREIGN KEY' and TABLE_NAME in ('Question', 'Answer')

TABLE_NAME  CONSTRAINT_NAME
----------  ------------------------------
Question    FK__Question__correc__656C112C
Answer      FK__Answer__question__66603565

(2 rows affected)

so we can use

    correct_answer_id = Column(
        Integer, ForeignKey("Answer.id", name="FK__Question__correc__656C112C")
    )

Sadly I didn't receive any answer and only got to a workaround. I cannot get the drop_all() method to work. However since this is something that shouldn't be done while in production, the workaround is just fine. This is the core piece of code that basically turns off the foreing key checks to delete the tables:

#DELETES ALL TABLES
with db.engine.connect() as conn:
    conn.execute("SET FOREIGN_KEY_CHECKS=0")
    inspector = inspect(db.engine)
    for table_name in inspector.get_table_names():
        stmt = "DROP TABLE "+table_name
        conn.execute(stmt)
    conn.execute("SET FOREIGN_KEY_CHECKS=1")

If you want some more context; this is the full test code:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Table, Column, Integer, String, CHAR, Boolean, DateTime, ForeignKey, inspect
from sqlalchemy.orm import relationship
import os

app = Flask(__name__,  instance_relative_config=True)
app.config.from_object('config') #Load the public config file
app.config.from_pyfile('config.py') #Load the private config file
username = os.getenv('username')
password = os.getenv('password')
app.config['SQLALCHEMY_DATABASE_URI']="mysql://" + app.config['USERNAME'] + ":" + app.config['PASSWORD'] + "@localhost/education"
app.config['DEVELOPMENT']=True
app.config['DEBUG']=True
app.secret_key = os.environ.get("SECRET_KEY") or os.urandom(24)

#Initialize database
db = SQLAlchemy(app)

class Question(db.Model):
    __tablename__ = "Question"
    id = Column(Integer, primary_key=True)
    question = Column(String(256),nullable=False)
    correct_answer_id = Column(Integer,ForeignKey('Answer.id'))

    correct_answer = relationship("Answer", foreign_keys=[correct_answer_id])
    answers = relationship("Question", foreign_keys="[Answer.question_id]")


class Answer(db.Model):
    __tablename__ = "Answer"
    id = Column(Integer, primary_key=True)
    question_id = Column(Integer, ForeignKey('Question.id'),nullable=False)
    answer = Column(String(256))

#DELETES ALL TABLES
with db.engine.connect() as conn:
    conn.execute("SET FOREIGN_KEY_CHECKS=0")
    inspector = inspect(db.engine)
    for table_name in inspector.get_table_names():
        stmt = "DROP TABLE "+table_name
        conn.execute(stmt)
    conn.execute("SET FOREIGN_KEY_CHECKS=1")


#CREATES ALL TABLES
db.create_all()

EDIT: I'll leave this answer here as it also works, however I think the accepted answer should be your preferred method.

Related