Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: db migration #3595

Merged
merged 34 commits into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
34 commits
Select commit Hold shift click to select a range
df09d08
feat(sqlalchemy): Replace peewee with sqlalchemy
jonathan-rohde Jun 18, 2024
bee835c
feat(sqlalchemy): remove session reference from router
jonathan-rohde Jun 21, 2024
070d908
feat(sqlalchemy): use subprocess to do migrations
jonathan-rohde Jun 24, 2024
320e658
feat(sqlalchemy): cleanup fixes
jonathan-rohde Jun 24, 2024
c134eab
feat(sqlalchemy): format backend
jonathan-rohde Jun 24, 2024
eb01e8d
feat(sqlalchemy): use scoped session
jonathan-rohde Jun 24, 2024
da403f3
feat(sqlalchemy): use session factory instead of context manager
jonathan-rohde Jun 24, 2024
a9b1487
feat(sqlalchemy): fix wrong column types
jonathan-rohde Jun 24, 2024
8f939cf
feat(sqlalchemy): some fixes
jonathan-rohde Jun 24, 2024
2fb27ad
feat(sqlalchemy): add missing file
jonathan-rohde Jun 24, 2024
d88bd51
feat(sqlalchemy): format backend
jonathan-rohde Jun 24, 2024
642c352
feat(sqlalchemy): rebase
jonathan-rohde Jun 25, 2024
d4b6b7c
feat(sqlalchemy): reverted not needed api change
jonathan-rohde Jun 25, 2024
23e4d9d
feat(sqlalchemy): formatting
jonathan-rohde Jun 25, 2024
827b1e5
feat(sqlalchemy): execute tests in github actions
jonathan-rohde Jun 25, 2024
df47c49
Merge branch 'refs/heads/dev' into feat/sqlalchemy-instead-of-peewee
jonathan-rohde Jun 28, 2024
5391f4c
feat(sqlalchemy): add new column
jonathan-rohde Jun 28, 2024
2aecd7d
Merge branch 'refs/heads/dev' into feat/sqlalchemy-instead-of-peewee
jonathan-rohde Jul 1, 2024
d0e89a0
Merge pull request #3327 from jonathan-rohde/feat/sqlalchemy-instead-…
tjbck Jul 2, 2024
647aa19
chore: format
tjbck Jul 2, 2024
44a9b86
fix: functions
tjbck Jul 3, 2024
aa88022
fix: functions
tjbck Jul 3, 2024
4d23957
revert: model_validate
tjbck Jul 3, 2024
0d78b63
Merge pull request #3621 from open-webui/dev
tjbck Jul 4, 2024
15f6f7b
revert: peewee migrations
tjbck Jul 4, 2024
bfc53b4
revert
tjbck Jul 4, 2024
1b65df3
revert
tjbck Jul 4, 2024
8646460
refac
tjbck Jul 4, 2024
37a5d2c
Update db.py
tjbck Jul 4, 2024
8fe2a7b
fix
tjbck Jul 4, 2024
8b13755
Update auths.py
tjbck Jul 4, 2024
d60f066
Merge pull request #3668 from open-webui/dev
tjbck Jul 6, 2024
1436bb7
enh: handle peewee migration
tjbck Jul 6, 2024
4e75150
Merge pull request #3669 from open-webui/dev-migration-session
tjbck Jul 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 67,28 @@ jobs:
path: compose-logs.txt
if-no-files-found: ignore

pytest:
name: Run backend tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r backend/requirements.txt

- name: pytest run
run: |
ls -al
cd backend
PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO

migration_test:
name: Run Migration Tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -171,7 193,7 @@ jobs:
fi

# Check that service will reconnect to postgres when connection will be closed
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health)
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
if [[ "$status_code" -ne 200 ]] ; then
echo "Server has failed before postgres reconnect check"
exit 1
Expand All @@ -183,7 205,7 @@ jobs:
cur = conn.cursor(); \
cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"

status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health)
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
if [[ "$status_code" -ne 200 ]] ; then
echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
exit 1
Expand Down
114 changes: 114 additions & 0 deletions backend/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 1,114 @@
# A generic, single database configuration.

[alembic]
# path to migration scripts
script_location = migrations

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s

# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .

# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =

# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions

# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.

# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

# sqlalchemy.url = REPLACE_WITH_DATABASE_URL


[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples

# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME

# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
58 changes: 32 additions & 26 deletions backend/apps/webui/internal/db.py
Original file line number Diff line number Diff line change
@@ -1,18 1,35 @@
import os
import logging
import json
from contextlib import contextmanager
from typing import Optional, Any
from typing_extensions import Self

from peewee import *
from peewee_migrate import Router
from sqlalchemy import create_engine, types, Dialect
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.sql.type_api import _T

from apps.webui.internal.wrappers import register_connection
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR

log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])


class JSONField(TextField):
class JSONField(types.TypeDecorator):
impl = types.Text
cache_ok = True

def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any:
return json.dumps(value)

def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any:
if value is not None:
return json.loads(value)

def copy(self, **kw: Any) -> Self:
return JSONField(self.impl.length)

def db_value(self, value):
return json.dumps(value)

Expand All @@ -29,26 46,15 @@ def python_value(self, value):
else:
pass


# The `register_connection` function encapsulates the logic for setting up
# the database connection based on the connection string, while `connect`
# is a Peewee-specific method to manage the connection state and avoid errors
# when a connection is already open.
try:
DB = register_connection(DATABASE_URL)
log.info(f"Connected to a {DB.__class__.__name__} database.")
except Exception as e:
log.error(f"Failed to initialize the database connection: {e}")
raise

router = Router(
DB,
migrate_dir=BACKEND_DIR / "apps" / "webui" / "internal" / "migrations",
logger=log,
SQLALCHEMY_DATABASE_URL = DATABASE_URL
if "sqlite" in SQLALCHEMY_DATABASE_URL:
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
else:
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
)
router.run()
try:
DB.connect(reuse_if_open=True)
except OperationalError as e:
log.info(f"Failed to connect to database again due to: {e}")
pass
Base = declarative_base()
Session = scoped_session(SessionLocal)
Original file line number Diff line number Diff line change
@@ -1,10 1,7 @@
"""Peewee migrations -- 017_add_user_oauth_sub.py.

Some examples (model - class or model name)::

> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name

> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
Expand All @@ -21,7 18,6 @@
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)

"""

from contextlib import suppress
Expand Down
21 changes: 0 additions & 21 deletions backend/apps/webui/internal/migrations/README.md

This file was deleted.

72 changes: 0 additions & 72 deletions backend/apps/webui/internal/wrappers.py

This file was deleted.

2 changes: 1 addition & 1 deletion backend/apps/webui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 3,7 @@
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware

from sqlalchemy.orm import Session
from apps.webui.routers import (
auths,
users,
Expand Down
Loading
Loading