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.
@@ -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