ENUM type in SQLAlchemy with PostgreSQL - postgresql

I'm using SQLAlchemy core with a postgresql database and I would like to add the ENUM type to my table definition. According to the postgresql documentation, the ENUM type must be defined prior to the table being created:
CREATE TYPE gender_enum AS ENUM ('female', 'male');
CREATE TABLE person (
name VARCHAR(20),
gender gender_enum
);
The problem is when I'm creating the table definition. After reading the SQLAlchemy documentation I couldn't find any implementation examples. I've tried something like this but it didn't work:
from sqlalchemy.dialects.postgresql import ENUM
person = Table('user_profile', metadata,
Column('name', String(20)),
Column('gender', ENUM('female', 'male'))
);
How it must be done?

You need to import Enum from sqlalchemy and add a name to it. It should work like this:
from sqlalchemy import Enum
person = Table("user_profile", metadata,
Column("name", String(20)),
Column("gender", Enum("female", "male", name="gender_enum", create_type=False))
);

#Tim's answer is definitely correct but I wanted to offer the way I setup my ENUMs.
In my models.py I will create the values as tuples
skill_levels = ('Zero', 'A little', 'Some', 'A lot')
Then I will create a skill_level_enum variable and assign it an ENUM class with the skill level values as args.
skill_level_enum = ENUM(*skill_levels, name="skill_level")
In my table model then I pass in the skill_level_enum
class User(db.Model):
id = db.Column(db.Integer(), primary_key=True)
skill_level = db.Column(skill_level_enum)
I have found this makes my code a lot cleaner and I'm able to make updates to the skill_levels at the top of my file rather than scanning my models for the right thing to update.

You can use Python's native enums as the column type as well:
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.dialects.postgresql import ENUM as pgEnum
from enum import Enum, unique
#unique
class errorTypeEnum(Enum):
videoValidation = 'videoValidation'
audioValidation = 'audioValidation'
subtitleValidation = 'subtitleValidation'
db = SQLAlchemy()
class Error(db.Model):
serviceID = db.Column(db.String(20), primary_key=True)
timestamp = db.Column(db.DateTime, unique=False, nullable=False)
category = db.Column(pgEnum(errorTypeEnum), unique=False, nullable=False)

The code below worked for me on SQLAlchemy 1.3.11 and Postgres 12.0.
You must first create the Enum type in postgres before creating the user table. This can be done directly through sql statement.
CREATE TYPE permission AS ENUM ('READ_ONLY', 'READ_WRITE', 'ADMIN', 'OWNER');
Then set up the project model
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
__tablename__ = 'user'
user_id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(200), nullable=False)
access = db.Column(db.Enum('READ_ONLY', 'READ_WRITE', 'ADMIN', 'OWNER', name="permission"))

Related

Postgres connection string with schema, alembic sqlalchemy

I am working on Alembic with SQLAlchemy on my Alembic.ini I am setting my postgres connection string but it goes to my public schema, i need to choose my schema.
How I can use an specific schema:
alembic.ini file
sqlalchemy.url = postgresql://myuser:mypassword#server:host/database
models.py
import datetime
from sqlalchemy import Column,Integer,String,DateTime,create_engine
from sqlalchemy.orm import declarative_base,sessionmaker
Base = declarative_base()
class UserModel(Base):
__tablename__='myschema.person'
id = Column(Integer, primary_key=True)
first_name = Column(String, nullable=False)
last_name = Column(String, nullable = False)
birth = Column(DateTime)
This connection string works with public, I need to connect to my "sales" schema.
On my models.py I am just creating a table person, I tried adding there Core schema but didn't works
Regards
Here you can find a complete working example
Define schema on your model
class A(Base):
__tablename__ = 'a'
__table_args__ = {
"schema": "alphabet"
}
edit env.py to work with schemas
with connectable.connect() as connection:
"""
Configure migration context
1. Pass our models metadata
2. Set schema for alembic_version table
3. Load all available schemas
"""
context.configure(
connection=connection,
target_metadata=target_metadata,
"""
here you are passing schema
"""
version_table_schema=target_metadata.schema,
include_schemas=True
)
Run alembic revision and upgrade and it will work

SQLAlchemy Joined Table Inheritance: Problem adding objects

I'm having problems trying to add inherited objects with SQLAlchemy following the instructions in https://docs.sqlalchemy.org/en/14/orm/inheritance.html to implement Joined Table Inheritance.
I'm using a PostgreSQL version 14 as a database engine.
Here is my Base configuration:
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.automap import automap_base
db_string = os.environ['DB_STRING']
engine = create_engine(db_string)
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
Base = automap_base()
Base.query = db_session.query_property()
Base.prepare(engine, reflect=True)
Here is Instrument (parent class) definition:
from sqlalchemy import Column, String
from context_model.database import Base
class Instrument(Base):
__tablename__ = "instrument"
_id = Column(String, primary_key=True)
discriminator = Column(String)
__mapper_args__ = {
'polymorphic_identity': 'instrument',
'polymorphic_on': discriminator
Here is Bond (child class) definition:
import datetime as dt
from sqlalchemy import Column, Integer, Float, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from context_model.instrument.instrument import Instrument
class Bond(Instrument):
__tablename__ = "bond"
_id = Column(String, ForeignKey("instrument._id"), primary_key=True)
_provider_bbg_id = Column(String)
__mapper_args__ = {
'polymorphic_identity': 'bond'
}
When I try to add a Bond instance and persist it to the database:
Bond = Base.classes.bond
bond = Bond()
bond._id = "0"
bond._provider_bbg_id = "XXX"
db_session.add(bond)
db_session.commit()
appears the following error message:
sqlalchemy.exc.IntegrityError: (psycopg2.errors.ForeignKeyViolation) insert or update on table "bond" violates foreign key constraint "bond__id_fkey"
DETAIL: Key (_id)=(0) is not present in table "instrument"."
It seems to me that the inheritance is not working for some reason, did I define well the Instrument (parent) and Bond (child) classes ?, maybe do I need to use another type of Base ? (I'm using automap_base )
Thanks in advance for your help!
I don't know the exact reasons but I changed from declarative_base() to automap_base() as #snakecharmerb suggested in the commentaries and now it works :)

Conflicts with relationship between tables

I've been constantly getting a warning on the console and I'm going crazy from how much I've been reading but I haven't been able to resolve this:
SAWarning: relationship 'Book.users' will copy column user.uid to column user_book.uid, which conflicts with relationship(s): 'User.books' (copies user.uid to user_book.uid). If this is not intention, consider if these relationships should be linked with back_populates, or if viewonly=True should be applied to one or more if they are read-only. For the less common case that foreign key constraints are partially overlapping, the orm.foreign() annotation can be used to isolate the columns that should be written towards. The 'overlaps' parameter may be used to remove this warning.
The tables the console cites in this notice are as follows:
user_book = db.Table('user_book',
db.Column('uid', db.Integer, db.ForeignKey('user.uid'), primary_key=True),
db.Column('bid', db.Text, db.ForeignKey('book.bid'), primary_key=True),
db.Column('date_added', db.DateTime(timezone=True), server_default=db.func.now())
)
class User(db.Model):
__tablename__ = 'user'
uid = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(25), nullable=False)
hash = db.Column(db.String(), nullable=False)
first_name = db.Column(db.String(30), nullable=True)
last_name = db.Column(db.String(80), nullable=True)
books = db.relationship('Book', secondary=user_book)
class Book(db.Model):
__tablename__ = 'book'
bid = db.Column(db.Text, primary_key=True)
title = db.Column(db.Text, nullable=False)
authors = db.Column(db.Text, nullable=False)
thumbnail = db.Column(db.Text, nullable=True)
users = db.relationship('User', secondary=user_book)
I use the user_book table to show the user the books he has added.
What am I missing? I take this opportunity to ask, semantically the relationship between tables and foreign keys is being done correctly?
As the warning message suggests, you are missing the back_populates= attributes in your relationships:
class User(db.Model):
# …
books = db.relationship('Book', secondary=user_book, back_populates="users")
# …
class Book(db.Model):
# …
users = db.relationship('User', secondary=user_book, back_populates="books")
# …
I kind of figure this out.
As the code in official tutorial.
from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String(64))
kw = relationship("Keyword", secondary=lambda: user_keyword_table)
def __init__(self, name):
self.name = name
class Keyword(Base):
__tablename__ = "keyword"
id = Column(Integer, primary_key=True)
keyword = Column("keyword", String(64))
def __init__(self, keyword):
self.keyword = keyword
user_keyword_table = Table(
"user_keyword",
Base.metadata,
Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
Column("keyword_id", Integer, ForeignKey("keyword.id"), primary_key=True),
)
Doesn't it make you wander why the relationship only exists in User class rather than both class ?
The thing is, it automatically creates the reverse relationship in Keyword class (a "backref='users' liked parameter is required I supposed ?)

How to create a mssql VIEW model using flask-migrate?

Mssql+pyodbc connection.
Imports and db
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Table, text, Text, Unicode, MetaData
from sqlalchemy.orm import relationship
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
I have generated table models using flask-sqlacodegen which look like this
class BlaBla(db.Model):
__tablename__ = 'bla_bla'
blabla= db.Column(db.Integer, primary_key=True)
bla2= db.Column(db.Integer, nullable=False)
The view that I have in the db was generated like this
t_bla_bla_bla_view = Table(
'View_bla_bla_bla',
Column('irrelevant', db.Unicode(50), nullable=False),
Column('etc', db.Unicode(50), nullable=False),
Column('etc2', db.Unicode(50), nullable=False)
)
When I run flask db migrate, the tables are properly created, while the View is also created as a table.
What is the actual syntax on creating a view?
Maybe there is a raw query migrate workaround?

How do I prevent sql alchemy from inserting the None value to field?

The Alembic migration script :
def upgrade():
uuid_gen = saexp.text("UUID GENERATE V1MC()")
op.create_table(
'foo',
sa.Column('uuid', UUID, primary_key=True, server_default=uuid_gen),
sa.Column(
'inserted',
sa.DateTime(timezone=True),
server_default=sa.text("not null now()"))
sa.Column('data', sa.Text)
)
This is my Base class for SQL Alchemy:
Class Foo(Base):
__tablename__ = 'foo'
inserted = Column(TIMESTAMP)
uuid = Column(UUID, primary_key=True)
data = Column(TEXT)
It has a static mehtod for insert :
#staticmethod
def insert(session, jsondata):
foo = Foo()
foo.data = jsondata['data']
if 'inserted' in jsondata:
foo.inserted = jsondata['inserted']
if 'uuid' in jsondata:
foo.uuid = jsondata['uuid']
session.add(foo)
return foo
the purpose of the 2 if's are to simplify testing. this way i can "inject" a uuid and inserted date, to get predictible data for my tests
When trying to insert data
foo = Foo()
foo.insert(session, {"data": "foo bar baz"})
session.commit()
I get an IntegrityError :
[SQL: 'INSERT INTO foo (inserted, data) VALUES (%(inserted)s, %(data)s) RETURNING foo.uuid'] [parameters: {'data': 'foo bar baz', 'inserted': None}]
wich seem normal to me because the insert violates the "not-null" constraint in the postgres database.
How do I prevent sql alchemy from inserting the None value to the inserted field ?
While playing and testing around, I found that if the "inserted" column is defined as primary key , sql alchemy does not include the field in the insert statement.
def upgrade():
uuid_gen = saexp.text("UUID GENERATE V1MC()")
op.create_table(
'foo',
sa.Column('uuid', UUID, primary_key=True, server_default=uuid_gen),
sa.Column(
'inserted',
primary_key=True,
sa.DateTime(timezone=True),
server_default=sa.text("not null now()"))
sa.Column('data', sa.Text)
)
But this is not what I want.
The primary problem is the server_default which is missing in the inserted member in class Foo. It's only present in the alembic script. Note that the alembic definitions are only used when running the migrations. They do not affect the application. For this reason, it's a good idea to copy the exact same definitions from the alembic script to your application (or vice-versa).
Because no value is defined in the model definition, sqlalchemy seems to set this to None when the class is instantiated. This will then be sent to the DB which will complain. To fix this, either set default or server_default on the model definition (the class inheriting from Base).
Some additional notes/questions:
Where does UUID GENERATE V1MC() come from? The official docs look different. I replaced it with func.uuid_generate_v1mc().
The server_default value in your case contains not null which is incorrect. You should set nullable=False on you column attribute (see below).
alembic script
# revision identifiers, used by Alembic.
revision = THIS_IS_DIFFERENT_ON_EACH_INSTANCE! # '1b7e145f2138'
down_revision = None
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
def upgrade():
op.create_table(
'foo',
sa.Column('uuid', UUID, primary_key=True,
server_default=sa.func.uuid_generate_v1mc()),
sa.Column(
'inserted',
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text("now()")),
sa.Column('data', sa.Text)
)
def downgrade():
op.drop_table('foo')
tester.py
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, create_engine, func
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.dialects.postgresql import (
TEXT,
TIMESTAMP,
UUID,
)
engine = create_engine('postgresql://michel#/michel')
Session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
Base = declarative_base()
class Foo(Base):
__tablename__ = 'foo'
inserted = Column(TIMESTAMP, nullable=False,
server_default=func.now())
uuid = Column(UUID, primary_key=True,
server_default=func.uuid_generate_v1mc()),
data = Column(TEXT)
#staticmethod
def insert(session, jsondata):
foo = Foo()
foo.data = jsondata['data']
if 'inserted' in jsondata:
foo.inserted = jsondata['inserted']
if 'uuid' in jsondata:
foo.uuid = jsondata['uuid']
session.add(foo)
return foo
if __name__ == '__main__':
session = Session()
Foo.insert(session, {"data": "foo bar baz"})
session.commit()
session.close()
output after execution
[9:43:54] michel#BBS-nexus [1 background job(s)]
/home/users/michel/tmp› psql -c "select * from foo"
uuid | inserted | data
--------------------------------------+-------------------------------+-------------
71f5fd32-0602-11e6-aebb-27be4bbac26e | 2016-04-19 09:43:45.297191+02 | foo bar baz
(1 row)