Skip to content

Commit 4f023f7

Browse files
Add db empty check constraints and migration
1 parent 809a062 commit 4f023f7

File tree

2 files changed

+83
-1
lines changed

2 files changed

+83
-1
lines changed

app/models.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
from datetime import datetime
22

3-
from sqlalchemy import Index
3+
from sqlalchemy import CheckConstraint, Index
44
from werkzeug.security import generate_password_hash, check_password_hash
55
from email_validator import validate_email, EmailNotValidError
66
from email_normalize import normalize
77

88
from app import db
99

1010

11+
class ConstraintFactory:
12+
@staticmethod
13+
def non_empty_string(column_name):
14+
constraint_name = f'{column_name}_non_empty'
15+
return CheckConstraint(f"TRIM({column_name}) != ''", name=constraint_name)
16+
17+
1118
class User(db.Model):
1219
__tablename__ = 'user'
1320
id = db.Column(db.Integer, primary_key=True)
@@ -16,6 +23,12 @@ class User(db.Model):
1623
password_hash = db.Column(db.String(256), nullable=False)
1724
created_on = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
1825

26+
__table_args__ = (
27+
ConstraintFactory.non_empty_string('email'),
28+
ConstraintFactory.non_empty_string('email_normalized'),
29+
ConstraintFactory.non_empty_string('password_hash')
30+
)
31+
1932
# Does not check for non-deliverable mails. Use check_deliverability or resolve for that which does DNS checks
2033
# For more stricter validation, use confirmation emails, or a third party API
2134
@staticmethod
@@ -68,6 +81,10 @@ class Category(db.Model):
6881
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
6982
subcategories = db.relationship("Subcategory", secondary=category_subcategory, back_populates="categories", lazy='dynamic', passive_deletes=True)
7083

84+
__table_args__ = (
85+
ConstraintFactory.non_empty_string('name'),
86+
)
87+
7188
def to_json(self):
7289
return {
7390
'id': self.id,
@@ -84,6 +101,10 @@ class Subcategory(db.Model):
84101
categories = db.relationship("Category", secondary=category_subcategory, back_populates="subcategories", lazy='dynamic', passive_deletes=True)
85102
products = db.relationship("Product", secondary=subcategory_product, back_populates="subcategories", lazy='dynamic', passive_deletes=True)
86103

104+
__table_args__ = (
105+
ConstraintFactory.non_empty_string('name'),
106+
)
107+
87108
def to_json(self):
88109
return {
89110
'id': self.id,
@@ -100,6 +121,10 @@ class Product(db.Model):
100121
created_at = db.Column(db.DateTime, nullable=False ,default=datetime.utcnow)
101122
subcategories = db.relationship("Subcategory", secondary=subcategory_product, back_populates="products", lazy='dynamic', passive_deletes=True)
102123

124+
__table_args__ = (
125+
ConstraintFactory.non_empty_string('name'),
126+
)
127+
103128
def to_json(self):
104129
return {
105130
'id': self.id,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""add constraints for empty strings
2+
3+
Revision ID: 911b11318ef1
4+
Revises: 27b56cc8451c
5+
Create Date: 2025-09-19 20:25:48.290940
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '911b11318ef1'
14+
down_revision = '27b56cc8451c'
15+
branch_labels = None
16+
depends_on = None
17+
18+
# migrations generated manually! Alembic did not these detect changes.
19+
20+
def upgrade():
21+
# User table constraints
22+
with op.batch_alter_table('user', schema=None) as batch_op:
23+
batch_op.create_check_constraint(batch_op.f('user_email_non_empty_check'), "trim(email) <> ''")
24+
batch_op.create_check_constraint(batch_op.f('user_email_normalized_non_empty_check'), "trim(email_normalized) <> ''")
25+
batch_op.create_check_constraint(batch_op.f('user_password_hash_non_empty_check'), "trim(password_hash) <> ''")
26+
27+
# Category table constraint
28+
with op.batch_alter_table('category', schema=None) as batch_op:
29+
batch_op.create_check_constraint(batch_op.f('category_name_non_empty_check'), "trim(name) <> ''")
30+
31+
# Subcategory table constraint
32+
with op.batch_alter_table('subcategory', schema=None) as batch_op:
33+
batch_op.create_check_constraint(batch_op.f('subcategory_name_non_empty_check'), "trim(name) <> ''")
34+
35+
# Product table constraint
36+
with op.batch_alter_table('product', schema=None) as batch_op:
37+
batch_op.create_check_constraint(batch_op.f('product_name_non_empty_check'), "trim(name) <> ''")
38+
39+
40+
def downgrade():
41+
# Product table constraint
42+
with op.batch_alter_table('product', schema=None) as batch_op:
43+
batch_op.drop_constraint(batch_op.f('product_name_non_empty_check'), type_='check')
44+
45+
# Subcategory table constraint
46+
with op.batch_alter_table('subcategory', schema=None) as batch_op:
47+
batch_op.drop_constraint(batch_op.f('subcategory_name_non_empty_check'), type_='check')
48+
49+
# Category table constraint
50+
with op.batch_alter_table('category', schema=None) as batch_op:
51+
batch_op.drop_constraint(batch_op.f('category_name_non_empty_check'), type_='check')
52+
53+
# User table constraints
54+
with op.batch_alter_table('user', schema=None) as batch_op:
55+
batch_op.drop_constraint(batch_op.f('user_password_hash_non_empty_check'), type_='check')
56+
batch_op.drop_constraint(batch_op.f('user_email_normalized_non_empty_check'), type_='check')
57+
batch_op.drop_constraint(batch_op.f('user_email_non_empty_check'), type_='check')

0 commit comments

Comments
 (0)