open-webui-sqlite-migration 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- open_webui_sqlite_migration-0.1.0/LICENSE +21 -0
- open_webui_sqlite_migration-0.1.0/PKG-INFO +47 -0
- open_webui_sqlite_migration-0.1.0/README.md +29 -0
- open_webui_sqlite_migration-0.1.0/open_webui_sqlite_migration/__init__.py +1 -0
- open_webui_sqlite_migration-0.1.0/open_webui_sqlite_migration/migrate.py +213 -0
- open_webui_sqlite_migration-0.1.0/pyproject.toml +25 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Digitalist Open Cloud
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: open-webui-sqlite-migration
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Migrate Open WebUI from SQLite to PostgreSQL
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Mikke Schirén
|
|
7
|
+
Author-email: mikke.schiren@digitalist.com
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: psycopg2-binary (>=2.9.11)
|
|
15
|
+
Requires-Dist: rich (>=13.9.4)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Migrate tool for OpenWebUI SQLite
|
|
19
|
+
|
|
20
|
+
Migrate from using SQLite database in Open WebUI to use Postgres.
|
|
21
|
+
|
|
22
|
+
Inspiration from: <https://github.com/taylorwilsdon/open-webui-postgres-migration>,
|
|
23
|
+
one of the big differences is that the migration require no input, with environment
|
|
24
|
+
variables set, you just run the script, no input.
|
|
25
|
+
|
|
26
|
+
This so you can automate the process, instead of manual input.
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Before anything else, backup you SQLite database and keep it in a safe place.
|
|
31
|
+
|
|
32
|
+
Needed environment variables:
|
|
33
|
+
|
|
34
|
+
- `SQLITE_DB_PATH` - exact path to your open webui db, like: `/app/backend/data/webui.db`.
|
|
35
|
+
- `MIGRATE_DATABASE_URL` - normally the same you should use for `DATABASE_URL`, like `postgresql://user:pass@postgres:5432/openwebui`
|
|
36
|
+
|
|
37
|
+
Also you need to start Open WebUI with `DATABASE_URL`, so needed tables are created. After that,
|
|
38
|
+
you remove that variable so you go back to use SQLite. When using SQLite, you run the migration script,
|
|
39
|
+
then you stop Open WebUI, and then again set `DATABASE_URL`. If everything now runs smoothly, you can remove the
|
|
40
|
+
SQLite database. Keep a backup of the database until you are really sure that all things are working as they
|
|
41
|
+
should.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT
|
|
46
|
+
Copyright (c) Digitalist Open Cloud.
|
|
47
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Migrate tool for OpenWebUI SQLite
|
|
2
|
+
|
|
3
|
+
Migrate from using SQLite database in Open WebUI to use Postgres.
|
|
4
|
+
|
|
5
|
+
Inspiration from: <https://github.com/taylorwilsdon/open-webui-postgres-migration>,
|
|
6
|
+
one of the big differences is that the migration require no input, with environment
|
|
7
|
+
variables set, you just run the script, no input.
|
|
8
|
+
|
|
9
|
+
This so you can automate the process, instead of manual input.
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Before anything else, backup you SQLite database and keep it in a safe place.
|
|
14
|
+
|
|
15
|
+
Needed environment variables:
|
|
16
|
+
|
|
17
|
+
- `SQLITE_DB_PATH` - exact path to your open webui db, like: `/app/backend/data/webui.db`.
|
|
18
|
+
- `MIGRATE_DATABASE_URL` - normally the same you should use for `DATABASE_URL`, like `postgresql://user:pass@postgres:5432/openwebui`
|
|
19
|
+
|
|
20
|
+
Also you need to start Open WebUI with `DATABASE_URL`, so needed tables are created. After that,
|
|
21
|
+
you remove that variable so you go back to use SQLite. When using SQLite, you run the migration script,
|
|
22
|
+
then you stop Open WebUI, and then again set `DATABASE_URL`. If everything now runs smoothly, you can remove the
|
|
23
|
+
SQLite database. Keep a backup of the database until you are really sure that all things are working as they
|
|
24
|
+
should.
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
MIT
|
|
29
|
+
Copyright (c) Digitalist Open Cloud.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ["migrate"]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SQLite to PostgreSQL migration for Open WebUI
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
import sqlite3
|
|
9
|
+
import csv
|
|
10
|
+
import argparse
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, Iterable, List
|
|
13
|
+
from io import StringIO
|
|
14
|
+
|
|
15
|
+
import psycopg2
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_args():
|
|
25
|
+
parser = argparse.ArgumentParser(
|
|
26
|
+
description="SQLite → PostgreSQL migration for Open WebUI"
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--dry-run",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Validate and preview migration without writing to PostgreSQL",
|
|
32
|
+
)
|
|
33
|
+
return parser.parse_args()
|
|
34
|
+
|
|
35
|
+
ARGS = parse_args()
|
|
36
|
+
DRY_RUN = ARGS.dry_run
|
|
37
|
+
|
|
38
|
+
def env(key: str, default=None, *, required=False, cast=str):
|
|
39
|
+
value = os.getenv(key, default)
|
|
40
|
+
if required and value is None:
|
|
41
|
+
raise RuntimeError(f"Missing environment variable: {key}")
|
|
42
|
+
try:
|
|
43
|
+
return cast(value) if value is not None else value
|
|
44
|
+
except Exception:
|
|
45
|
+
raise RuntimeError(f"Invalid value for {key}: {value}")
|
|
46
|
+
|
|
47
|
+
SQLITE_PATH = Path(env("SQLITE_DB_PATH", required=True))
|
|
48
|
+
MIGRATE_DATABASE_URL = env("MIGRATE_DATABASE_URL", required=True)
|
|
49
|
+
BATCH_SIZE = env("BATCH_SIZE", 5000, cast=int)
|
|
50
|
+
|
|
51
|
+
def validate_sqlite(path: Path) -> None:
|
|
52
|
+
with sqlite3.connect(path) as conn:
|
|
53
|
+
conn.execute("PRAGMA integrity_check")
|
|
54
|
+
|
|
55
|
+
def validate_postgres(db_url: str) -> None:
|
|
56
|
+
conn = psycopg2.connect(db_url)
|
|
57
|
+
conn.close()
|
|
58
|
+
|
|
59
|
+
def pg_ident(name: str) -> str:
|
|
60
|
+
if name.lower() in {"user", "group", "order", "table", "select"}:
|
|
61
|
+
return f'"{name}"'
|
|
62
|
+
return name
|
|
63
|
+
|
|
64
|
+
def sqlite_tables(conn: sqlite3.Connection) -> List[str]:
|
|
65
|
+
cur = conn.execute(
|
|
66
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
67
|
+
)
|
|
68
|
+
return [
|
|
69
|
+
r[0] for r in cur.fetchall()
|
|
70
|
+
if r[0] not in {"alembic_version", "migratehistory"}
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
def sqlite_schema(conn: sqlite3.Connection, table: str):
|
|
74
|
+
return conn.execute(f'PRAGMA table_info("{table}")').fetchall()
|
|
75
|
+
|
|
76
|
+
def pg_column_types(conn, table: str) -> Dict[str, str]:
|
|
77
|
+
with conn.cursor() as cur:
|
|
78
|
+
cur.execute("""
|
|
79
|
+
SELECT column_name, data_type
|
|
80
|
+
FROM information_schema.columns
|
|
81
|
+
WHERE table_schema = 'public'
|
|
82
|
+
AND table_name = %s
|
|
83
|
+
""", (table,))
|
|
84
|
+
return dict(cur.fetchall())
|
|
85
|
+
|
|
86
|
+
def stream_sqlite_rows(
|
|
87
|
+
conn: sqlite3.Connection,
|
|
88
|
+
table: str,
|
|
89
|
+
columns: List[str],
|
|
90
|
+
) -> Iterable[tuple]:
|
|
91
|
+
col_sql = ", ".join(f'"{c}"' for c in columns)
|
|
92
|
+
cur = conn.execute(f'SELECT {col_sql} FROM "{table}"')
|
|
93
|
+
while True:
|
|
94
|
+
rows = cur.fetchmany(BATCH_SIZE)
|
|
95
|
+
if not rows:
|
|
96
|
+
break
|
|
97
|
+
for row in rows:
|
|
98
|
+
yield row
|
|
99
|
+
|
|
100
|
+
def normalize_row(row, columns, pg_types):
|
|
101
|
+
out = []
|
|
102
|
+
for value, col in zip(row, columns):
|
|
103
|
+
col_type = pg_types.get(col)
|
|
104
|
+
if value is None:
|
|
105
|
+
out.append(None)
|
|
106
|
+
elif col_type == "jsonb":
|
|
107
|
+
if isinstance(value, (dict, list)):
|
|
108
|
+
out.append(json.dumps(value))
|
|
109
|
+
else:
|
|
110
|
+
try:
|
|
111
|
+
json.loads(value)
|
|
112
|
+
out.append(value)
|
|
113
|
+
except Exception:
|
|
114
|
+
out.append("{}")
|
|
115
|
+
else:
|
|
116
|
+
out.append(value)
|
|
117
|
+
return tuple(out)
|
|
118
|
+
|
|
119
|
+
def migrate_table(sqlite_conn: sqlite3.Connection, pg_conn, table: str):
|
|
120
|
+
sqlite_count = sqlite_conn.execute(
|
|
121
|
+
f'SELECT COUNT(*) FROM "{table}"'
|
|
122
|
+
).fetchone()[0]
|
|
123
|
+
|
|
124
|
+
console.print(
|
|
125
|
+
f"[cyan]Table:[/] {table} "
|
|
126
|
+
f"[dim](rows: {sqlite_count})[/]"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if DRY_RUN:
|
|
130
|
+
console.print(f"[yellow]DRY‑RUN: for {table}[/]")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
schema = sqlite_schema(sqlite_conn, table)
|
|
134
|
+
columns = [c[1] for c in schema]
|
|
135
|
+
pg_types = pg_column_types(pg_conn, table)
|
|
136
|
+
|
|
137
|
+
rows = (
|
|
138
|
+
normalize_row(row, columns, pg_types)
|
|
139
|
+
for row in stream_sqlite_rows(sqlite_conn, table, columns)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
buffer = StringIO()
|
|
143
|
+
writer = csv.writer(
|
|
144
|
+
buffer,
|
|
145
|
+
lineterminator="\n",
|
|
146
|
+
quoting=csv.QUOTE_MINIMAL,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
for row in rows:
|
|
150
|
+
writer.writerow("" if v is None else v for v in row)
|
|
151
|
+
|
|
152
|
+
buffer.seek(0)
|
|
153
|
+
|
|
154
|
+
with pg_conn.cursor() as cur:
|
|
155
|
+
cur.execute(f"TRUNCATE TABLE {pg_ident(table)} CASCADE")
|
|
156
|
+
cur.copy_expert(
|
|
157
|
+
f"COPY {pg_ident(table)} ({', '.join(columns)}) FROM STDIN WITH CSV",
|
|
158
|
+
buffer,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
pg_conn.commit()
|
|
162
|
+
|
|
163
|
+
def main():
|
|
164
|
+
console.print(
|
|
165
|
+
Panel(
|
|
166
|
+
f"SQLite → PostgreSQL Migration "
|
|
167
|
+
f"{'(DRY‑RUN)' if DRY_RUN else ''}",
|
|
168
|
+
style="cyan",
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
validate_sqlite(SQLITE_PATH)
|
|
173
|
+
validate_postgres(MIGRATE_DATABASE_URL)
|
|
174
|
+
|
|
175
|
+
sqlite_conn = sqlite3.connect(SQLITE_PATH)
|
|
176
|
+
sqlite_conn.text_factory = lambda b: b.decode("utf-8", errors="replace")
|
|
177
|
+
|
|
178
|
+
pg_conn = psycopg2.connect(MIGRATE_DATABASE_URL)
|
|
179
|
+
|
|
180
|
+
if DRY_RUN:
|
|
181
|
+
with pg_conn.cursor() as cur:
|
|
182
|
+
cur.execute("SET default_transaction_read_only = on")
|
|
183
|
+
console.print("[yellow]DRY‑RUN: PostgreSQL session is read‑only[/]")
|
|
184
|
+
else:
|
|
185
|
+
with pg_conn.cursor() as cur:
|
|
186
|
+
cur.execute("SET session_replication_role = replica")
|
|
187
|
+
pg_conn.commit()
|
|
188
|
+
|
|
189
|
+
tables = sqlite_tables(sqlite_conn)
|
|
190
|
+
|
|
191
|
+
with Progress(
|
|
192
|
+
SpinnerColumn(),
|
|
193
|
+
TextColumn("[progress.description]{task.description}"),
|
|
194
|
+
BarColumn(),
|
|
195
|
+
) as progress:
|
|
196
|
+
task = progress.add_task("Processing tables...", total=len(tables))
|
|
197
|
+
for table in tables:
|
|
198
|
+
migrate_table(sqlite_conn, pg_conn, table)
|
|
199
|
+
progress.advance(task)
|
|
200
|
+
|
|
201
|
+
sqlite_conn.close()
|
|
202
|
+
|
|
203
|
+
if not DRY_RUN:
|
|
204
|
+
with pg_conn.cursor() as cur:
|
|
205
|
+
cur.execute("SET session_replication_role = origin")
|
|
206
|
+
pg_conn.commit()
|
|
207
|
+
|
|
208
|
+
pg_conn.close()
|
|
209
|
+
|
|
210
|
+
console.print(Panel("✅ Done", style="green"))
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
main()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "open-webui-sqlite-migration"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Migrate Open WebUI from SQLite to PostgreSQL"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Mikke Schirén", email = "mikke.schiren@digitalist.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
"psycopg2-binary>=2.9.11",
|
|
14
|
+
"rich>=13.9.4"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
open-webui-migrate-sqlite = "open_webui_sqlite_migration.migrate:main"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
22
|
+
build-backend = "poetry.core.masonry.api"
|
|
23
|
+
|
|
24
|
+
[tool.poetry]
|
|
25
|
+
package-mode = true
|