Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
"""Add timezone support for asyncpg compatibility

Revision ID: c8613607a100
Revises: 182e5471b900
Create Date: 2025-11-07 14:56:02.303392

"""

from collections.abc import Sequence

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "c8613607a100" # pragma: allowlist secret
down_revision: str | None = "182e5471b900"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
conn = op.get_bind()

# Only apply changes if using PostgreSQL
# SQLite and other databases don't have timezone-aware datetime types
if conn.dialect.name == "postgresql":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Cristhianzl let's add a paragraph explaining why we're doing this migration somewhere in this file. I think that'll be good practice for future changes, and easier than digging through commit history to find a PR description (especially if we do any refactoring).

# Alter datetime columns to use TIMESTAMP WITH TIME ZONE

# User table
op.alter_column(
"user",
"create_at",
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False),
existing_nullable=False,
)
op.alter_column(
"user",
"updated_at",
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False),
existing_nullable=False,
)
op.alter_column(
"user",
"last_login_at",
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False),
existing_nullable=True,
)

# ApiKey table
op.alter_column(
"apikey",
"last_used_at",
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False),
existing_nullable=True,
)

# Flow table
op.alter_column(
"flow",
"updated_at",
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False),
existing_nullable=True,
)

# Message table
op.alter_column(
"message",
"timestamp",
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False),
existing_nullable=False,
)

# File table
op.alter_column(
"file",
"created_at",
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False),
existing_nullable=False,
)
op.alter_column(
"file",
"updated_at",
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False),
existing_nullable=False,
)

# Transaction table
op.alter_column(
"transaction",
"timestamp",
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False),
existing_nullable=False,
)

# VertexBuild table
op.alter_column(
"vertex_build",
"timestamp",
type_=sa.DateTime(timezone=True),
existing_type=sa.DateTime(timezone=False),
existing_nullable=False,
)


def downgrade() -> None:
conn = op.get_bind()

# Only apply changes if using PostgreSQL
if conn.dialect.name == "postgresql":
# Revert datetime columns to TIMESTAMP WITHOUT TIME ZONE

# User table
op.alter_column(
"user",
"create_at",
type_=sa.DateTime(timezone=False),
existing_type=sa.DateTime(timezone=True),
existing_nullable=False,
)
op.alter_column(
"user",
"updated_at",
type_=sa.DateTime(timezone=False),
existing_type=sa.DateTime(timezone=True),
existing_nullable=False,
)
op.alter_column(
"user",
"last_login_at",
type_=sa.DateTime(timezone=False),
existing_type=sa.DateTime(timezone=True),
existing_nullable=True,
)

# ApiKey table
op.alter_column(
"apikey",
"last_used_at",
type_=sa.DateTime(timezone=False),
existing_type=sa.DateTime(timezone=True),
existing_nullable=True,
)

# Flow table
op.alter_column(
"flow",
"updated_at",
type_=sa.DateTime(timezone=False),
existing_type=sa.DateTime(timezone=True),
existing_nullable=True,
)

# Message table
op.alter_column(
"message",
"timestamp",
type_=sa.DateTime(timezone=False),
existing_type=sa.DateTime(timezone=True),
existing_nullable=False,
)

# File table
op.alter_column(
"file",
"created_at",
type_=sa.DateTime(timezone=False),
existing_type=sa.DateTime(timezone=True),
existing_nullable=False,
)
op.alter_column(
"file",
"updated_at",
type_=sa.DateTime(timezone=False),
existing_type=sa.DateTime(timezone=True),
existing_nullable=False,
)

# Transaction table
op.alter_column(
"transaction",
"timestamp",
type_=sa.DateTime(timezone=False),
existing_type=sa.DateTime(timezone=True),
existing_nullable=False,
)

# VertexBuild table
op.alter_column(
"vertex_build",
"timestamp",
type_=sa.DateTime(timezone=False),
existing_type=sa.DateTime(timezone=True),
existing_nullable=False,
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def utc_now():

class ApiKeyBase(SQLModel):
name: str | None = Field(index=True, nullable=True, default=None)
last_used_at: datetime | None = Field(default=None, nullable=True)
last_used_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True), nullable=True), default=None)
total_uses: int = Field(default=0)
is_active: bool = Field(default=True)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import datetime, timezone
from uuid import UUID, uuid4

from sqlalchemy import Column, DateTime
from sqlmodel import Field, SQLModel, UniqueConstraint

from langflow.schema.serialize import UUIDstr
Expand All @@ -13,7 +14,11 @@ class File(SQLModel, table=True): # type: ignore[call-arg]
path: str = Field(nullable=False)
size: int = Field(nullable=False)
provider: str | None = Field(default=None)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = Field(
sa_column=Column(DateTime(timezone=True), nullable=False), default_factory=lambda: datetime.now(timezone.utc)
)
updated_at: datetime = Field(
sa_column=Column(DateTime(timezone=True), nullable=False), default_factory=lambda: datetime.now(timezone.utc)
)

__table_args__ = (UniqueConstraint("name", "user_id"),)
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from fastapi import HTTPException, status
from lfx.log.logger import logger
from pydantic import BaseModel, ValidationInfo, field_serializer, field_validator
from sqlalchemy import DateTime, Text, UniqueConstraint, text
from sqlalchemy import Enum as SQLEnum
from sqlalchemy import Text, UniqueConstraint, text
from sqlmodel import JSON, Column, Field, Relationship, SQLModel

from langflow.schema.data import Data
Expand Down Expand Up @@ -40,7 +40,9 @@ class FlowBase(SQLModel):
gradient: str | None = Field(default=None, nullable=True)
data: dict | None = Field(default=None, nullable=True)
is_component: bool | None = Field(default=False, nullable=True)
updated_at: datetime | None = Field(default_factory=lambda: datetime.now(timezone.utc), nullable=True)
updated_at: datetime | None = Field(
sa_column=Column(DateTime(timezone=True), nullable=True), default_factory=lambda: datetime.now(timezone.utc)
)
webhook: bool | None = Field(default=False, nullable=True, description="Can be used on the webhook endpoint")
endpoint_name: str | None = Field(default=None, nullable=True, index=True)
tags: list[str] | None = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from uuid import UUID, uuid4

from pydantic import ConfigDict, field_serializer, field_validator
from sqlalchemy import Text
from sqlalchemy import DateTime, Text
from sqlmodel import JSON, Column, Field, SQLModel

from langflow.schema.content_block import ContentBlock
Expand All @@ -17,7 +17,7 @@

class MessageBase(SQLModel):
timestamp: Annotated[datetime, str_to_timestamp_validator] = Field(
default_factory=lambda: datetime.now(timezone.utc)
sa_column=Column(DateTime(timezone=True), nullable=False), default_factory=lambda: datetime.now(timezone.utc)
)
sender: str
sender_name: str
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
from uuid import UUID, uuid4

from pydantic import field_serializer, field_validator
from sqlmodel import JSON, Column, Field, SQLModel
from sqlalchemy import Column, DateTime
from sqlmodel import JSON, Field, SQLModel

from langflow.serialization.serialization import get_max_items_length, get_max_text_length, serialize


class TransactionBase(SQLModel):
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
timestamp: datetime = Field(
sa_column=Column(DateTime(timezone=True), nullable=False), default_factory=lambda: datetime.now(timezone.utc)
)
vertex_id: str = Field(nullable=False)
target_id: str | None = Field(default=None)
inputs: dict | None = Field(default=None, sa_column=Column(JSON))
Expand Down
12 changes: 8 additions & 4 deletions src/backend/base/langflow/services/database/models/user/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from uuid import UUID, uuid4

from pydantic import BaseModel
from sqlalchemy import JSON, Column
from sqlalchemy import JSON, Column, DateTime
from sqlmodel import Field, Relationship, SQLModel

from langflow.schema.serialize import UUIDstr
Expand All @@ -29,9 +29,13 @@ class User(SQLModel, table=True): # type: ignore[call-arg]
profile_image: str | None = Field(default=None, nullable=True)
is_active: bool = Field(default=False)
is_superuser: bool = Field(default=False)
create_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
last_login_at: datetime | None = Field(default=None, nullable=True)
create_at: datetime = Field(
sa_column=Column(DateTime(timezone=True), nullable=False), default_factory=lambda: datetime.now(timezone.utc)
)
updated_at: datetime = Field(
sa_column=Column(DateTime(timezone=True), nullable=False), default_factory=lambda: datetime.now(timezone.utc)
)
last_login_at: datetime | None = Field(sa_column=Column(DateTime(timezone=True), nullable=True), default=None)
api_keys: list["ApiKey"] = Relationship(
back_populates="user",
sa_relationship_kwargs={"cascade": "delete"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
from uuid import UUID, uuid4

from pydantic import BaseModel, field_serializer, field_validator
from sqlalchemy import Text
from sqlalchemy import DateTime, Text
from sqlmodel import JSON, Column, Field, SQLModel

from langflow.serialization.serialization import get_max_items_length, get_max_text_length, serialize


class VertexBuildBase(SQLModel):
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
timestamp: datetime = Field(
sa_column=Column(DateTime(timezone=True), nullable=False), default_factory=lambda: datetime.now(timezone.utc)
)
id: str = Field(nullable=False)
data: dict | None = Field(default=None, sa_column=Column(JSON))
artifacts: dict | None = Field(default=None, sa_column=Column(JSON))
Expand Down
1 change: 1 addition & 0 deletions src/backend/tests/integration/drivers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This file makes the directory a Python package
18 changes: 18 additions & 0 deletions src/backend/tests/integration/drivers/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Conftest for PostgreSQL driver tests.

This conftest overrides the _start_app fixture from the parent integration/conftest.py
to prevent it from loading the client fixture, which would override the database URL.
"""

import pytest


@pytest.fixture(autouse=True)
def _start_app():
"""Override parent _start_app fixture to skip client loading.

PostgreSQL driver tests create their own database connections
and don't need the app client fixture.
"""
# Do nothing - this prevents the parent fixture from running
return
Comment on lines +1 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add __init__.py to resolve namespace package error.

The static analysis tool and pipeline both flag that this file is part of an implicit namespace package. To resolve this, create an __init__.py file in the src/backend/tests/integration/drivers/ directory.

To fix this, create an empty __init__.py file:

#!/bin/bash
touch src/backend/tests/integration/drivers/__init__.py
🧰 Tools
🪛 GitHub Actions: Ruff Style Check

[error] 1-1: INP001: File is part of an implicit namespace package. Add an init.py.

🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 1-1: Ruff (INP001)
src/backend/tests/integration/drivers/conftest.py:1:1: INP001 File src/backend/tests/integration/drivers/conftest.py is part of an implicit namespace package. Add an __init__.py.

🤖 Prompt for AI Agents
In src/backend/tests/integration/drivers/conftest.py lines 1-18: static analysis
reports this module is part of an implicit namespace package; fix by adding an
empty __init__.py in the same directory
(src/backend/tests/integration/drivers/__init__.py) so the package is explicit
and import resolution/static analysis pass; create the file with normal text
file permissions (empty content is fine).

Loading
Loading