docs: Update README.md for PostgreSQL requirement and remove SQLite references

This commit updates the README.md to reflect the requirement of PostgreSQL for both development and production environments. It clarifies the database setup instructions, removes references to SQLite, and ensures consistency in the documentation regarding database configurations. Additionally, it enhances the clarity of environment variable settings and database schema compatibility between the web and desktop versions.
This commit is contained in:
Tanya
2026-01-06 11:56:08 -05:00
parent b104dcba71
commit 1f3f35d535
5 changed files with 95 additions and 540 deletions
+10 -153
View File
@@ -10,7 +10,6 @@ from sqlalchemy import (
Column,
Date,
DateTime,
String,
ForeignKey,
Index,
Integer,
@@ -19,7 +18,6 @@ from sqlalchemy import (
Text,
UniqueConstraint,
CheckConstraint,
TypeDecorator,
)
from sqlalchemy.orm import declarative_base, relationship
@@ -31,147 +29,6 @@ if TYPE_CHECKING:
Base = declarative_base()
class PrismaCompatibleDateTime(TypeDecorator):
"""
DateTime type that stores in a format compatible with Prisma's SQLite driver.
Prisma's SQLite driver has issues with microseconds in datetime strings.
This type ensures datetimes are stored in ISO format without microseconds:
'YYYY-MM-DD HH:MM:SS' instead of 'YYYY-MM-DD HH:MM:SS.ffffff'
Uses String as the underlying type for SQLite to have full control over the format.
"""
impl = String
cache_ok = True
def process_bind_param(self, value, dialect):
"""Convert Python datetime to SQL string format without microseconds."""
if value is None:
return None
if isinstance(value, datetime):
# Strip microseconds and format as ISO string without microseconds
# This ensures Prisma can read it correctly
return value.replace(microsecond=0).strftime('%Y-%m-%d %H:%M:%S')
# If it's already a string, ensure it doesn't have microseconds
if isinstance(value, str):
try:
# Parse and reformat to remove microseconds
if '.' in value:
# Has microseconds or timezone info - strip them
dt = datetime.strptime(value.split('.')[0], '%Y-%m-%d %H:%M:%S')
elif 'T' in value:
# ISO format with T
dt = datetime.fromisoformat(value.replace('Z', '+00:00').split('.')[0])
else:
# Already in correct format
return value
return dt.strftime('%Y-%m-%d %H:%M:%S')
except (ValueError, TypeError):
# If parsing fails, return as-is
return value
return value
def process_result_value(self, value, dialect):
"""Convert SQL string back to Python datetime."""
if value is None:
return None
if isinstance(value, str):
# Parse ISO format string
try:
# Try parsing with microseconds first (for existing data)
if '.' in value:
return datetime.strptime(value.split('.')[0], '%Y-%m-%d %H:%M:%S')
else:
return datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
except ValueError:
# Fallback to ISO format parser
return datetime.fromisoformat(value.replace('Z', '+00:00'))
return value
class PrismaCompatibleDate(TypeDecorator):
"""
Date type that stores in DateTime format for Prisma compatibility.
Prisma's SQLite driver expects DateTime format (YYYY-MM-DD HH:MM:SS) even for dates.
This type stores dates with a time component (00:00:00) so Prisma can read them correctly,
while still using Python's date type in the application.
Uses String as the underlying type for SQLite to have full control over the format.
"""
impl = String
cache_ok = True
def process_bind_param(self, value, dialect):
"""Convert Python date to space-separated DateTime format for Prisma compatibility."""
if value is None:
return None
if isinstance(value, date):
# Store date in space-separated format: YYYY-MM-DD HH:MM:SS (matching date_added format)
return value.strftime('%Y-%m-%d 00:00:00')
if isinstance(value, datetime):
# If datetime is passed, extract date and format with time component
return value.date().strftime('%Y-%m-%d 00:00:00')
if isinstance(value, str):
# If it's already a string, ensure it's in space-separated format
try:
# Try to parse and convert to space-separated format
if 'T' in value:
# ISO format with T - convert to space-separated
date_part, time_part = value.split('T', 1)
time_part = time_part.split('+')[0].split('-')[0].split('Z')[0].split('.')[0]
if len(time_part.split(':')) == 3:
return f"{date_part} {time_part}"
else:
return f"{date_part} 00:00:00"
elif ' ' in value:
# Already space-separated - ensure it has time component
parts = value.split(' ', 1)
if len(parts) == 2:
date_part, time_part = parts
time_part = time_part.split('.')[0] # Remove microseconds if present
if len(time_part.split(':')) == 3:
return f"{date_part} {time_part}"
# Missing time component - add it
return f"{parts[0]} 00:00:00"
else:
# Just date (YYYY-MM-DD) - add time component
d = datetime.strptime(value, '%Y-%m-%d').date()
return d.strftime('%Y-%m-%d 00:00:00')
except (ValueError, TypeError):
# If parsing fails, return as-is
return value
return value
def process_result_value(self, value, dialect):
"""Convert SQL string back to Python date."""
if value is None:
return None
if isinstance(value, str):
# Extract date part from ISO 8601 or space-separated DateTime string
try:
if 'T' in value:
# ISO format with T
return datetime.fromisoformat(value.split('T')[0]).date()
elif ' ' in value:
# Space-separated format - extract date part
return datetime.strptime(value.split()[0], '%Y-%m-%d').date()
else:
# Just date (YYYY-MM-DD)
return datetime.strptime(value, '%Y-%m-%d').date()
except ValueError:
# Fallback to ISO format parser
try:
return datetime.fromisoformat(value.split('T')[0]).date()
except:
return datetime.strptime(value.split()[0], '%Y-%m-%d').date()
if isinstance(value, (date, datetime)):
if isinstance(value, datetime):
return value.date()
return value
return value
class Photo(Base):
"""Photo model - matches desktop schema exactly."""
@@ -180,8 +37,8 @@ class Photo(Base):
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
path = Column(Text, unique=True, nullable=False, index=True)
filename = Column(Text, nullable=False)
date_added = Column(PrismaCompatibleDateTime, default=datetime.utcnow, nullable=False)
date_taken = Column(PrismaCompatibleDate, nullable=True, index=True)
date_added = Column(DateTime, default=datetime.utcnow, nullable=False)
date_taken = Column(Date, nullable=True, index=True)
processed = Column(Boolean, default=False, nullable=False, index=True)
file_hash = Column(Text, nullable=True, index=True) # Nullable to support existing photos without hashes
media_type = Column(Text, default="image", nullable=False, index=True) # "image" or "video"
@@ -214,7 +71,7 @@ class Person(Base):
middle_name = Column(Text, nullable=True)
maiden_name = Column(Text, nullable=True)
date_of_birth = Column(Date, nullable=True)
created_date = Column(PrismaCompatibleDateTime, default=datetime.utcnow, nullable=False)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
faces = relationship("Face", back_populates="person")
person_encodings = relationship(
@@ -285,7 +142,7 @@ class PersonEncoding(Base):
quality_score = Column(Numeric, default=0.0, nullable=False, index=True)
detector_backend = Column(Text, default="retinaface", nullable=False)
model_name = Column(Text, default="ArcFace", nullable=False)
created_date = Column(PrismaCompatibleDateTime, default=datetime.utcnow, nullable=False)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
person = relationship("Person", back_populates="person_encodings")
face = relationship("Face", back_populates="person_encodings")
@@ -303,7 +160,7 @@ class Tag(Base):
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
tag_name = Column(Text, unique=True, nullable=False, index=True)
created_date = Column(PrismaCompatibleDateTime, default=datetime.utcnow, nullable=False)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
photo_tags = relationship(
"PhotoTagLinkage", back_populates="tag", cascade="all, delete-orphan"
@@ -322,7 +179,7 @@ class PhotoTagLinkage(Base):
Integer, default=0, nullable=False,
server_default="0"
)
created_date = Column(PrismaCompatibleDateTime, default=datetime.utcnow, nullable=False)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
photo = relationship("Photo", back_populates="photo_tags")
tag = relationship("Tag", back_populates="photo_tags")
@@ -343,7 +200,7 @@ class PhotoFavorite(Base):
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(Text, nullable=False, index=True)
photo_id = Column(Integer, ForeignKey("photos.id"), nullable=False, index=True)
created_date = Column(PrismaCompatibleDateTime, default=datetime.utcnow, nullable=False)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
photo = relationship("Photo", back_populates="favorites")
@@ -374,8 +231,8 @@ class User(Base):
index=True,
)
password_change_required = Column(Boolean, default=True, nullable=False, index=True)
created_date = Column(PrismaCompatibleDateTime, default=datetime.utcnow, nullable=False)
last_login = Column(PrismaCompatibleDateTime, nullable=True)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
last_login = Column(DateTime, nullable=True)
__table_args__ = (
Index("idx_users_username", "username"),
@@ -399,7 +256,7 @@ class PhotoPersonLinkage(Base):
photo_id = Column(Integer, ForeignKey("photos.id"), nullable=False, index=True)
person_id = Column(Integer, ForeignKey("people.id"), nullable=False, index=True)
identified_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
created_date = Column(PrismaCompatibleDateTime, default=datetime.utcnow, nullable=False)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
photo = relationship("Photo", back_populates="video_people")
person = relationship("Person", back_populates="video_photos")