Skip to content

Commit 9eab952

Browse files
authored
feat: postgresql migration and removal of sqlite in pytest (#546)
* feat: remove support of sqlite, 100% postgres * fix: more migration and make datetime timezone aware in postgres * fix: change how database is get, and use contextvar to have difference instance between different loops * test: properly use client fixture that handle lifetime/database connection * fix: add missing client fixture parameters to test functions This commit fixes NameError issues where test functions were trying to use the 'client' fixture but didn't have it as a parameter. The changes include: 1. Added 'client' parameter to test functions in: - test_transcripts_audio_download.py (6 functions including fixture) - test_transcripts_speaker.py (3 functions) - test_transcripts_upload.py (1 function) - test_transcripts_rtc_ws.py (2 functions + appserver fixture) 2. Resolved naming conflicts in test_transcripts_rtc_ws.py where both HTTP client and StreamClient were using variable name 'client'. StreamClient instances are now named 'stream_client' to avoid conflicts. 3. Added missing 'from reflector.app import app' import in rtc_ws tests. Background: Previously implemented contextvars solution with get_database() function resolves asyncio event loop conflicts in Celery tasks. The global client fixture was also created to replace manual AsyncClient instances, ensuring proper FastAPI application lifecycle management and database connections during tests. All tests now pass except for 2 pre-existing RTC WebSocket test failures related to asyncpg connection issues unrelated to these fixes. * fix: ensure task are correctly closed * fix: make separate event loop for the live server * fix: make default settings pointing at postgres * build: remove pytest-docker deps out of dev, just tests group
1 parent 6fb5cb2 commit 9eab952

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2658
-2375
lines changed

.github/workflows/db_migrations.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,40 @@ on:
1717
jobs:
1818
test-migrations:
1919
runs-on: ubuntu-latest
20+
services:
21+
postgres:
22+
image: postgres:17
23+
env:
24+
POSTGRES_USER: reflector
25+
POSTGRES_PASSWORD: reflector
26+
POSTGRES_DB: reflector
27+
ports:
28+
- 5432:5432
29+
options: >-
30+
--health-cmd pg_isready -h 127.0.0.1 -p 5432
31+
--health-interval 10s
32+
--health-timeout 5s
33+
--health-retries 5
34+
35+
env:
36+
DATABASE_URL: postgresql://reflector:reflector@localhost:5432/reflector
2037

2138
steps:
2239
- uses: actions/checkout@v4
2340

41+
- name: Install PostgreSQL client
42+
run: sudo apt-get update && sudo apt-get install -y postgresql-client | cat
43+
44+
- name: Wait for Postgres
45+
run: |
46+
for i in {1..30}; do
47+
if pg_isready -h localhost -p 5432; then
48+
echo "Postgres is ready"
49+
break
50+
fi
51+
echo "Waiting for Postgres... ($i)" && sleep 1
52+
done
53+
2454
- name: Install uv
2555
uses: astral-sh/setup-uv@v3
2656
with:

server/migrations/versions/62dea3db63a5_add_room_options.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def upgrade() -> None:
3232
sa.Column("user_id", sa.String(), nullable=True),
3333
sa.Column("room_id", sa.String(), nullable=True),
3434
sa.Column(
35-
"is_locked", sa.Boolean(), server_default=sa.text("0"), nullable=False
35+
"is_locked", sa.Boolean(), server_default=sa.text("false"), nullable=False
3636
),
3737
sa.Column("room_mode", sa.String(), server_default="normal", nullable=False),
3838
sa.Column(
@@ -53,12 +53,15 @@ def upgrade() -> None:
5353
sa.Column("user_id", sa.String(), nullable=False),
5454
sa.Column("created_at", sa.DateTime(), nullable=False),
5555
sa.Column(
56-
"zulip_auto_post", sa.Boolean(), server_default=sa.text("0"), nullable=False
56+
"zulip_auto_post",
57+
sa.Boolean(),
58+
server_default=sa.text("false"),
59+
nullable=False,
5760
),
5861
sa.Column("zulip_stream", sa.String(), nullable=True),
5962
sa.Column("zulip_topic", sa.String(), nullable=True),
6063
sa.Column(
61-
"is_locked", sa.Boolean(), server_default=sa.text("0"), nullable=False
64+
"is_locked", sa.Boolean(), server_default=sa.text("false"), nullable=False
6265
),
6366
sa.Column("room_mode", sa.String(), server_default="normal", nullable=False),
6467
sa.Column(

server/migrations/versions/74b2b0236931_add_transcript_source_kind.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@
2020

2121
def upgrade() -> None:
2222
# ### commands auto generated by Alembic - please adjust! ###
23+
sourcekind_enum = sa.Enum("room", "live", "file", name="sourcekind")
24+
sourcekind_enum.create(op.get_bind())
25+
2326
op.add_column(
2427
"transcript",
2528
sa.Column(
2629
"source_kind",
27-
sa.Enum("ROOM", "LIVE", "FILE", name="sourcekind"),
30+
sourcekind_enum,
2831
nullable=True,
2932
),
3033
)
@@ -43,6 +46,8 @@ def upgrade() -> None:
4346
def downgrade() -> None:
4447
# ### commands auto generated by Alembic - please adjust! ###
4548
op.drop_column("transcript", "source_kind")
49+
sourcekind_enum = sa.Enum(name="sourcekind")
50+
sourcekind_enum.drop(op.get_bind())
4651

4752

4853
# ### end Alembic commands ###

server/migrations/versions/99365b0cd87b_add_title_short_and_long_summary_and_.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def upgrade() -> None:
2222
# ### commands auto generated by Alembic - please adjust! ###
2323
op.execute(
2424
"UPDATE transcript SET events = "
25-
'REPLACE(events, \'"event": "SUMMARY"\', \'"event": "LONG_SUMMARY"\');'
25+
'REPLACE(events::text, \'"event": "SUMMARY"\', \'"event": "LONG_SUMMARY"\')::json;'
2626
)
2727
op.alter_column("transcript", "summary", new_column_name="long_summary")
2828
op.add_column("transcript", sa.Column("title", sa.String(), nullable=True))
@@ -34,7 +34,7 @@ def downgrade() -> None:
3434
# ### commands auto generated by Alembic - please adjust! ###
3535
op.execute(
3636
"UPDATE transcript SET events = "
37-
'REPLACE(events, \'"event": "LONG_SUMMARY"\', \'"event": "SUMMARY"\');'
37+
'REPLACE(events::text, \'"event": "LONG_SUMMARY"\', \'"event": "SUMMARY"\')::json;'
3838
)
3939
with op.batch_alter_table("transcript", schema=None) as batch_op:
4040
batch_op.alter_column("long_summary", nullable=True, new_column_name="summary")
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""datetime timezone
2+
3+
Revision ID: 9f5c78d352d6
4+
Revises: 8120ebc75366
5+
Create Date: 2025-08-13 19:18:27.113593
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
import sqlalchemy as sa
12+
from alembic import op
13+
from sqlalchemy.dialects import postgresql
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "9f5c78d352d6"
17+
down_revision: Union[str, None] = "8120ebc75366"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
with op.batch_alter_table("meeting", schema=None) as batch_op:
25+
batch_op.alter_column(
26+
"start_date",
27+
existing_type=postgresql.TIMESTAMP(),
28+
type_=sa.DateTime(timezone=True),
29+
existing_nullable=True,
30+
)
31+
batch_op.alter_column(
32+
"end_date",
33+
existing_type=postgresql.TIMESTAMP(),
34+
type_=sa.DateTime(timezone=True),
35+
existing_nullable=True,
36+
)
37+
38+
with op.batch_alter_table("meeting_consent", schema=None) as batch_op:
39+
batch_op.alter_column(
40+
"consent_timestamp",
41+
existing_type=postgresql.TIMESTAMP(),
42+
type_=sa.DateTime(timezone=True),
43+
existing_nullable=False,
44+
)
45+
46+
with op.batch_alter_table("recording", schema=None) as batch_op:
47+
batch_op.alter_column(
48+
"recorded_at",
49+
existing_type=postgresql.TIMESTAMP(),
50+
type_=sa.DateTime(timezone=True),
51+
existing_nullable=False,
52+
)
53+
54+
with op.batch_alter_table("room", schema=None) as batch_op:
55+
batch_op.alter_column(
56+
"created_at",
57+
existing_type=postgresql.TIMESTAMP(),
58+
type_=sa.DateTime(timezone=True),
59+
existing_nullable=False,
60+
)
61+
62+
with op.batch_alter_table("transcript", schema=None) as batch_op:
63+
batch_op.alter_column(
64+
"created_at",
65+
existing_type=postgresql.TIMESTAMP(),
66+
type_=sa.DateTime(timezone=True),
67+
existing_nullable=True,
68+
)
69+
70+
# ### end Alembic commands ###
71+
72+
73+
def downgrade() -> None:
74+
# ### commands auto generated by Alembic - please adjust! ###
75+
with op.batch_alter_table("transcript", schema=None) as batch_op:
76+
batch_op.alter_column(
77+
"created_at",
78+
existing_type=sa.DateTime(timezone=True),
79+
type_=postgresql.TIMESTAMP(),
80+
existing_nullable=True,
81+
)
82+
83+
with op.batch_alter_table("room", schema=None) as batch_op:
84+
batch_op.alter_column(
85+
"created_at",
86+
existing_type=sa.DateTime(timezone=True),
87+
type_=postgresql.TIMESTAMP(),
88+
existing_nullable=False,
89+
)
90+
91+
with op.batch_alter_table("recording", schema=None) as batch_op:
92+
batch_op.alter_column(
93+
"recorded_at",
94+
existing_type=sa.DateTime(timezone=True),
95+
type_=postgresql.TIMESTAMP(),
96+
existing_nullable=False,
97+
)
98+
99+
with op.batch_alter_table("meeting_consent", schema=None) as batch_op:
100+
batch_op.alter_column(
101+
"consent_timestamp",
102+
existing_type=sa.DateTime(timezone=True),
103+
type_=postgresql.TIMESTAMP(),
104+
existing_nullable=False,
105+
)
106+
107+
with op.batch_alter_table("meeting", schema=None) as batch_op:
108+
batch_op.alter_column(
109+
"end_date",
110+
existing_type=sa.DateTime(timezone=True),
111+
type_=postgresql.TIMESTAMP(),
112+
existing_nullable=True,
113+
)
114+
batch_op.alter_column(
115+
"start_date",
116+
existing_type=sa.DateTime(timezone=True),
117+
type_=postgresql.TIMESTAMP(),
118+
existing_nullable=True,
119+
)
120+
121+
# ### end Alembic commands ###

server/migrations/versions/a7122bc0b2ca_add_shared_rooms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def upgrade() -> None:
2525
sa.Column(
2626
"is_shared",
2727
sa.Boolean(),
28-
server_default=sa.text("0"),
28+
server_default=sa.text("false"),
2929
nullable=False,
3030
),
3131
)

server/migrations/versions/b0e5f7876032_add_meeting_is_active.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ def upgrade() -> None:
2323
with op.batch_alter_table("meeting", schema=None) as batch_op:
2424
batch_op.add_column(
2525
sa.Column(
26-
"is_active", sa.Boolean(), server_default=sa.text("1"), nullable=False
26+
"is_active",
27+
sa.Boolean(),
28+
server_default=sa.text("true"),
29+
nullable=False,
2730
)
2831
)
2932

server/migrations/versions/b9348748bbbc_reviewed.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def upgrade() -> None:
2323
op.add_column(
2424
"transcript",
2525
sa.Column(
26-
"reviewed", sa.Boolean(), server_default=sa.text("0"), nullable=False
26+
"reviewed", sa.Boolean(), server_default=sa.text("false"), nullable=False
2727
),
2828
)
2929
# ### end Alembic commands ###

server/pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ tests = [
5757
"httpx-ws>=0.4.1",
5858
"pytest-httpx>=0.23.1",
5959
"pytest-celery>=0.0.0",
60+
"pytest-docker>=3.2.3",
61+
"asgi-lifespan>=2.1.0",
6062
]
6163
aws = ["aioboto3>=11.2.0"]
6264
evaluation = [
@@ -86,7 +88,7 @@ source = ["reflector"]
8688

8789
[tool.pytest_env]
8890
ENVIRONMENT = "pytest"
89-
DATABASE_URL = "sqlite:///test.sqlite"
91+
DATABASE_URL = "postgresql://test_user:test_password@localhost:15432/reflector_test"
9092

9193
[tool.pytest.ini_options]
9294
addopts = "-ra -q --disable-pytest-warnings --cov --cov-report html -v"

server/reflector/db/__init__.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,47 @@
1+
import contextvars
2+
from typing import Optional
3+
14
import databases
25
import sqlalchemy
36

47
from reflector.events import subscribers_shutdown, subscribers_startup
58
from reflector.settings import settings
69

7-
database = databases.Database(settings.DATABASE_URL)
810
metadata = sqlalchemy.MetaData()
911

12+
_database_context: contextvars.ContextVar[Optional[databases.Database]] = (
13+
contextvars.ContextVar("database", default=None)
14+
)
15+
16+
17+
def get_database() -> databases.Database:
18+
"""Get database instance for current asyncio context"""
19+
db = _database_context.get()
20+
if db is None:
21+
db = databases.Database(settings.DATABASE_URL)
22+
_database_context.set(db)
23+
return db
24+
25+
1026
# import models
1127
import reflector.db.meetings # noqa
1228
import reflector.db.recordings # noqa
1329
import reflector.db.rooms # noqa
1430
import reflector.db.transcripts # noqa
1531

1632
kwargs = {}
17-
if "sqlite" in settings.DATABASE_URL:
18-
kwargs["connect_args"] = {"check_same_thread": False}
33+
if "postgres" not in settings.DATABASE_URL:
34+
raise Exception("Only postgres database is supported in reflector")
1935
engine = sqlalchemy.create_engine(settings.DATABASE_URL, **kwargs)
2036

2137

2238
@subscribers_startup.append
2339
async def database_connect(_):
40+
database = get_database()
2441
await database.connect()
2542

2643

2744
@subscribers_shutdown.append
2845
async def database_disconnect(_):
46+
database = get_database()
2947
await database.disconnect()

0 commit comments

Comments
 (0)