Adding UniqueKey constraint to a sqlite3 table with Flask-Migration fails with IntrgrityError - sqlite

So I using sqlite as my test database and have the following classes in my models.py
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True, index=True)
username = db.Column(db.String(40), unique=True, index=True)
password_hash = db.Column(db.String(256))
alternate_id = db.Column(db.String(100))
posts = db.relationship('Posts', backref='author', lazy=True)
def get_id(self):
return str(self.alternate_id)
def __init__(self, username, password):
self.username = username
self.password_hash = generate_password_hash(password)
self.alternate_id = my_serializer.dumps(
self.username + self.password_hash)
def verify_password(self, password):
if check_password_hash(self.password_hash, password):
return "True"
class Posts(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False, unique=True)
description = db.Column(db.String(1500))
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
def __init__(self, title, description, author_id):
self.title = title
self.description = description
self.author_id = author_id
I added the unique key constraint to column title in my Posts class and then was trying to update the schema using Flask-Migrate.
Initially I was getting the No support for ALTER of constraints in SQLite dialect errors since sqlite3 does not support it through alembic. So I looked the alembic documentation and found that you can actually do such migrations using the batch mode migrations. So I updated my migration script as below.
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("posts") as batch_op:
batch_op.create_unique_constraint('unique_title', ['title'])
# ### end Alembic commands ###
Now when I try to run flask db upgrade I get the following error
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: _alembic_tmp_posts.title [SQL: 'INSERT INTO
_alembic_tmp_posts (id, title, description, author_id) SELECT posts.id, posts.title, posts.description, posts.author_id \nFROM posts'] (Background on this error at: http://sqlalche.me/e/gkpj`)
I am not able to understand that why IntegrityError exception is being thrown because if I look at the insert statement the number of columns are same.
Does it have something to do with the authors_id column having a foreignkey constraint on it ?

The database table column on which I was adding the unique constraint had duplicate data and that was the reason I was getting the integrity error, I am just surprised why I didn't notice that earlier.
So once I removed one of the duplicate rows, the database upgrade was successful.

Related

SQLAlchemy: delete rows from associate table from many-to-many relationship

I noticed that when deleting one entry, the corresponding rows from the secondary table are not deleted. here my models:
cashflows_tags_table = Table(
"cashflows_tags",
Base.metadata,
Column("cashflow_id", ForeignKey("cashflows.id"),primary_key=True),
Column("tag_id", ForeignKey("tags.id"), primary_key=True),
)
class Tag(Base):
__tablename__ = "tags"
id = Column(Integer, primary_key=True)
tag = Column(String, nullable=False, unique=True)
class Cashflow(Base):
__tablename__ = "cashflows"
id = Column(Integer, primary_key=True)
date = Column(DateTime, nullable=False)
amount = Column(Float, nullable=False)
description = Column(String, nullable=False)
### Many to Many ###
tags = relationship("Tag", secondary=cashflows_tags_table)
#######
What I understand from the documentation (https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html#deleting-rows-from-the-many-to-many-table) is that the deletion should be propagated automatically. Additional cascade/delete/delete-orphan would cause the deletion of the elements on the other "many" side, which is not what I want.
I am using sqlite, and the following ORM syntax for the deletion:
with Session(db_engine) as session:
ddd = delete(Cashflow).where(Cashflow.id.in_(id_list))
session.execute(ddd)
session.commit()
EDIT
in the end I solved it manually selecting the entry and emptying the tag collection before the deletion (as suggested in other threads). I will still leave the question open, since it is not clear to me if this is the expected behaviour
If you don't want to set a cascade, session.delete will remove both the CashFlow and the associated many-to-many records.
cfs = s.scalars(sa.select(Cashflow).where(Cashflow.id.in_(id_list)))
for cs in cfs:
session.delete(cf)
It will emit a DELETE statement for the many-to-many records first, then the CashFlows.
Alternatively, you can enable foreign keys in SQLite and set "CASCADE" as the ondelete behaviour for the foreign key in the association table - only rows associated with the CashFlow to be deleted will be deleted when using sqlachemy.delete.
from sqlalchemy.engine import Engine
# Enable foreign keys (from the SQLAlchemy docs)
#sa.event.listens_for(Engine, 'connect')
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute('PRAGMA foreign_keys=ON')
cursor.close()
cashflows_tags_table = sa.Table(
'cashflows_tags',
Base.metadata,
sa.Column('cashflow_id', sa.ForeignKey('cashflows.id', ondelete='CASCADE'), primary_key=True),
sa.Column('tag_id', sa.ForeignKey('tags.id'), primary_key=True),
)

How to get SQLAlchemy to create a class from a view instead of a table?

I am using flask-sqlalchemy and I want to create a class from a view instead of a db table. Is there an alternative to tablename? 'Car' was recently changed from a table to a view and now it's stuck sending a request.
class car(db.Model):
__tablename__ = 'car'
model = Column(Text, primary_key=True)
brand = Column(Text, primary_key=True)
condition = Column(Text, primary_key=True)
year = Column(Integer)
SQLAlchemy does not have a particular problem with ORM objects based on views. For example, this works fine with SQL Server because SQL Server allows DML (INSERT, UPDATE, DELETE) on views:
# set up test environment
with engine.begin() as conn:
conn.exec_driver_sql("DROP TABLE IF EXISTS car_table")
conn.exec_driver_sql("CREATE TABLE car_table (id integer primary key, make varchar(50))")
conn.exec_driver_sql("INSERT INTO car_table (id, make) VALUES (1, 'Audi'), (2, 'Buick')")
conn.exec_driver_sql("DROP VIEW IF EXISTS car_view")
conn.exec_driver_sql("CREATE VIEW car_view AS SELECT * FROM car_table WHERE id <> 2")
Base = sa.orm.declarative_base()
class Car(Base):
__tablename__ = "car_view"
id = Column(Integer, primary_key=True, autoincrement=False)
make = Column(String(50), nullable=False)
def __repr__(self):
return f"<Car(id={self.id}, make='{self.make}')>"
with Session(engine) as session:
print(session.execute(select(Car)).all())
# [(<Car(id=1, make='Audi')>,)]
# (note: the view excludes the row (object) where id == 2)
session.add(Car(id=3, make="Chevrolet"))
session.commit()
print(session.execute(select(Car)).all())
# [(<Car(id=1, make='Audi')>,), (<Car(id=3, make='Chevrolet')>,)]
However, if you really are using SQLite then you won't be able to add, update, or delete objects using a class based on a view because SQLite doesn't allow that:
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) cannot modify car_view because it is a view
[SQL: INSERT INTO car_view (id, make) VALUES (?, ?)]
[parameters: (3, 'Chevrolet')]
(Background on this error at: https://sqlalche.me/e/14/e3q8)

Why is SQLAlchemy expecting 6 rows here when I want to delete two?

I am using Flask-SQLAlchemy and I have the following tables:
tags = db.Table('tags',
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True),
db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True))
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(250))
body = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
author = db.Column(db.String(64))
tags = db.relationship('Tag', secondary=tags, lazy='subquery',
backref=db.backref('posts', lazy=True))
published = db.Column(db.Boolean)
slug = db.Column(db.String(300), unique=True, index=True)
def save(self):
if not self.slug:
self.slug = re.sub('[^\w]+', '-', self.title.lower())
def update_time(self):
self.timestamp = datetime.utcnow()
def __repr__(self):
return '<Post {}>'.format(self.title)
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
tag = db.Column(db.String(64), index=True, unique=True)
def __repr__(self):
return '<Tag {}>'.format(self.tag)
Currently I am making a function to allow the user to delete a post entirely with db.session.delete(post) and I am getting the follwing error:
sqlalchemy.orm.exc.StaleDataError: DELETE statement on table 'tags' expected to delete 6 row(s); Only 2 were matched.
Why does the delete statement expect 6 rows? 2 is the correct number and corresponds to how many entries there are in the tags table for that post, two unique tags attached to this post.
It seems the issue here was with my route and not my models.
Bringing the delete function code to the beginning of the route gave me the expected behaviour; the post was deleted from the database and associations in the helper table also removed.
The error was caused by some code in my route that appended selected tags from the submitted form to post.tags multiple times over resulting in the expectation of 6 rows.

Migrate Sqlalchemy schema from mssql to sqlite db?

I have all my table classes written for mssql but now I want to test my application locally so I need sqlitedb.Is there a way through which I can Replicate my database in sqlite.
I am facing some issues like sqlite does not support Float as a Primary key.I have more than 200 tables I can not go and edit all just for testing.I can have all the tables in one metadata.
My idea is to use sqlite just for testing and for production I will still be using mssql.
Note I changed Float to Integer but still my tables are not created instead it just creates a empty db.
My code
for table in metadata.tables:
keys_to_change = []
for pkey_column in metadata.tables[table].primary_key.columns.keys():
keys_to_change.append(pkey_column)
for data in list(metadata.tables[table].foreign_keys):
keys_to_change.append(data.column.name)
for column in metadata.tables[table].columns:
if column.name in keys_to_change:
if str(column.type) == 'FLOAT':
column.type = INTEGER
engine = create_engine('sqlite:///mytest.db', echo=True, echo_pool=True)
metadata.create_all(engine)
If you are able to change the model code, I would suggest to create an alias to the Float and use it to define those primary_key and ForeignKey columns, which you could just change for your sqlite testing:
# CONFIGURATION
PKType = Float # default: MSSQL; or Float(N, M)
# PKType = Integer # uncomment this for sqlite
and your model becomes like below:
class MyParent(Base):
__tablename__ = 'my_parent'
id = Column(PKType, primary_key=True)
name = Column(String)
children = relationship('MyChild', backref='parent')
class MyChild(Base):
__tablename__ = 'my_child'
id = Column(PKType, primary_key=True)
parent_id = Column(PKType, ForeignKey('my_parent.id'))
name = Column(String)
Alternatively, if you would like to be only changing the engine and not another configuration variable, you can use dialect-specific custom type handling:
import sqlalchemy.types as types
class PKType(types.TypeDecorator):
impl = Float
def load_dialect_impl(self, dialect):
if dialect.name == 'sqlite':
return dialect.type_descriptor(Integer())
else:
return dialect.type_descriptor(Float())

ProgrammingError Thread error in SQLAlchemy

I have a two simple tables in a sqlite db.
from sqlalchemy import MetaData, Table, Column, Integer, ForeignKey, \
create_engine, String
from sqlalchemy.orm import mapper, relationship, sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
engine = create_engine('sqlite:///dir_graph.sqlite', echo=True)
session_factory = sessionmaker(bind=engine)
Session = scoped_session(session_factory)
session = Session()
Base = declarative_base()
class NodeType(Base):
__tablename__ = 'nodetype'
id = Column(Integer, primary_key=True)
name = Column(String(20), unique=True)
nodes = relationship('Node', backref='nodetype')
def __init__(self, name):
self.name = name
def __repr__(self):
return "Nodetype: %s" % self.name
class Node(Base):
__tablename__ = 'node'
id = Column(Integer, primary_key=True)
name = Column(String(20), unique=True)
type_id = Column(Integer,
ForeignKey('nodetype.id'))
def __init__(self, _name, _type_id):
self.name = _name
self.type_id = _type_id
Base.metadata.create_all(engine)
After the run I interact with the interpreter. e.g. n1= Node('Node1',1) to learn about sqlalchemy. After I did a session.commit() and try another statement e.g. n2 = Node('n2',1) I get this error:
sqlalchemy.exc.ProgrammingError: (ProgrammingError) SQLite objects created in a thread can only be used in that same thread.The object was created in thread id 3932 and this is thread id 5740 None None.
How can I continue a session after I did a commit ?
tnx
SQLite by default prohibits the usage of a single connection in more than one thread.
just add connect_args={'check_same_thread': False} parameter to your engine variable like
engine = create_engine('sqlite:///dir_graph.sqlite', connect_args={'check_same_thread': False}, echo=True)
According to sqlite3.connect:
By default, check_same_thread is True and only the creating thread may
use the connection. If set False, the returned connection may be
shared across multiple threads. When using multiple threads with the
same connection writing operations should be serialized by the user to
avoid data corruption.

Resources