Reputation: 92149
This may be totally stupid thing to ask but I have such a requirement in my model where atleast either category
or parent_category
is not null
My model looks like
class BudgetCategories(db.Model):
__tablename__ = 'budget_categories'
uuid = Column('uuid', GUID(), default=uuid.uuid4, primary_key=True,
unique=True)
budget_id = Column(GUID(), ForeignKey('budgets.uuid'), nullable=False)
budget = relationship('Budget', backref='budgetCategories')
category = Column('category', sa.types.String, nullable=True)
parent_category = Column('parent_category', sa.types.String, nullable=True)
amount = Column('amount', Numeric(10, 2), nullable=False)
recurring = Column('recurring', sa.types.Boolean,
nullable=False)
created_on = Column('created_on', sa.types.DateTime(timezone=True),
nullable=False)
How can I specify that. I don't even know what to try
Any pointers appreciated
I am using PostgreSQL
as the backend database
Upvotes: 20
Views: 11325
Reputation: 673
I came across this problem some weeks ago, too and was provided with the solution here. So translated to sqlalchemy I'd guess (not yet tested!!):
from sqlalchemy.schema import CheckConstraint
class MyClass(Base):
__table_args__ = (
(CheckConstraint(
'num_nulls(nullif(trim(col_a::text), ''), nullif(trim(col_b::text), '')) = 1',
name='uuid_xor_notebook_path')),
)
id = Column(Integer, primary_key=True)
col_a = Column(String, nullable=True, unique=True)
col_b = Column(String, nullable=True, unique=True)
However for aesthetic reasons I prefer @andilabs solution. To still take into account empty fields for this reason, one could leverage another solution from here and write:
from sqlalchemy.schema import CheckConstraint
class MyClass(Base):
__table_args__ = (
(CheckConstraint(
'(col_a::text IS NULL) <> (col_b IS NULL)',
name='uuid_xor_notebook_path')),
)
id = Column(Integer, primary_key=True)
col_a = Column(String, nullable=True, unique=True)
col_b = Column(String, nullable=True, unique=True)
@validates('col_a', 'col_b')
def empty_string_to_null(self, key, value):
if isinstance(value, str) and value == '':
return None
else:
return value
Not really the shortest of all snippets, I admit...
Upvotes: 0
Reputation: 23
When using the PostgreSQL backend, you can also use the num_nonnulls function for this purpose:
class BudgetCategories(Base):
__tablename__ = 'budget_categories'
__table_args__ = (
CheckConstraint('num_nonnulls(category, parent_category) = 1'),
)
Upvotes: 2
Reputation: 23351
I needed XOR behavior in my SQLalchemy models. I come up with the following definition (backend used: PostgreSQL):
from sqlalchemy.schema import (
CheckConstraint
)
class ScheduledNotebook(Base):
__table_args__ = (
(CheckConstraint('(uuid::text IS NULL) <> (notebook_path IS NULL)', name='uuid_xor_notebook_path')),
)
id = Column(Integer, primary_key=True)
notebook_path = Column(String, nullable=True, unique=True)
uuid = Column(UUID(as_uuid=True), primary_key=True, unique=True, nullable=True)
and following alembic migration (note: autogenerate won't detect it - you have to add it manually):
def upgrade():
op.create_check_constraint(
'uuid_xor_notebook_path',
table_name='scheduled_notebooks',
schema='metadata',
condition='(uuid::text IS NULL) <> (notebook_path IS NULL)'
)
def downgrade():
op.drop_constraint('uuid_xor_notebook_path')
and it works like a charm:
- only notebook_path - OK
datalake=# INSERT INTO scheduled_notebooks (schedule,enabled,owner, notebook_path) VALUES ('{"kind":"hourly"}',true,'akos', '/a/b/c/d/e.ipynb');
INSERT 0 1
- only uuid - OK
datalake=# INSERT INTO scheduled_notebooks (schedule,enabled,owner, uuid) VALUES ('{"kind":"hourly"}',true,'akos', '7792bd5f-5819-45bf-8902-8cf43102434d');
INSERT 0 1
- both uuid and notebook_path - FAILS as desired
datalake=# INSERT INTO scheduled_notebooks (schedule,enabled,owner, uuid, notebook_path) VALUES ('{"kind":"hourly"}',true,'akos', '7792bd5f-5819-45bf-8902-8cf43102434f', '/a/b/c/d');
ERROR: new row for relation "scheduled_notebooks" violates check constraint "uuid_xor_notebook_path"
DETAIL: Failing row contains (567, /a/b/c/d, {"kind": "hourly"}, t, akos, null, null, null, 7792bd5f-5819-45bf-8902-8cf43102434f).
- neither uuid nor notebook_path - FAILS as desired
datalake=# INSERT INTO scheduled_notebooks (schedule,enabled,owner) VALUES ('{"kind":"hourly"}',true,'akos');
ERROR: new row for relation "scheduled_notebooks" violates check constraint "uuid_xor_notebook_path"
DETAIL: Failing row contains (568, null, {"kind": "hourly"}, t, akos, null, null, null, null).
Upvotes: 10
Reputation: 186
I hope is not too late but this should do the trick and it is checked to work with a PostGreSQL DB:
class BudgetCategories(Base):
__tablename__ = 'budget_categories'
__table_args__ = (
CheckConstraint('coalesce(category , parent_category ) is not null'),
)
# ...
Upvotes: 3
Reputation: 77082
I am not 100% sure about the PostgreSQL
syntax, but following addition to your BudgetCategories
model should do the trick using CheckConstraint
:
class BudgetCategories(Base):
__tablename__ = 'budget_categories'
# ...
# @note: new
__table_args__ = (
CheckConstraint('NOT(category IS NULL AND parent_category IS NULL)'),
)
Upvotes: 23