Last active
May 12, 2026 19:09
-
-
Save chmouel/990472b0775b5c6bdc17ea86f8213e1c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run --script | |
| # /// script | |
| # dependencies = ["psycopg2-binary"] | |
| # /// | |
| """Migrate atuin server.db (SQLite) -> PostgreSQL.""" | |
| import sqlite3 | |
| import sys | |
| import uuid | |
| import psycopg2 | |
| import psycopg2.extras | |
| SQLITE_PATH = "~/.local/share/atuin/server.db" | |
| PG_DSN = "postgres://atuin:zepass@localhost:5432/atuin" | |
| BATCH_SIZE = 5000 | |
| def to_uuid(val): | |
| if val is None: | |
| return None | |
| if isinstance(val, bytes): | |
| return str(uuid.UUID(bytes=val)) | |
| return str(uuid.UUID(str(val))) | |
| def migrate_users(sq, pg): | |
| cur = sq.execute("SELECT id, username, email, password, created_at, verified_at FROM users") | |
| rows = cur.fetchall() | |
| if not rows: | |
| print("users: 0 rows, skipping") | |
| return | |
| with pg.cursor() as pgc: | |
| psycopg2.extras.execute_values( | |
| pgc, | |
| """ | |
| INSERT INTO users (id, username, email, password, created_at, verified_at) | |
| VALUES %s | |
| ON CONFLICT DO NOTHING | |
| """, | |
| rows, | |
| ) | |
| pg.commit() | |
| print(f"users: migrated {len(rows)} row(s)") | |
| def migrate_sessions(sq, pg): | |
| cur = sq.execute("SELECT id, user_id, token FROM sessions") | |
| rows = cur.fetchall() | |
| if not rows: | |
| print("sessions: 0 rows, skipping") | |
| return | |
| with pg.cursor() as pgc: | |
| psycopg2.extras.execute_values( | |
| pgc, | |
| "INSERT INTO sessions (id, user_id, token) VALUES %s ON CONFLICT DO NOTHING", | |
| rows, | |
| ) | |
| pg.commit() | |
| print(f"sessions: migrated {len(rows)} row(s)") | |
| def migrate_store(sq, pg): | |
| total = sq.execute("SELECT COUNT(*) FROM store").fetchone()[0] | |
| print(f"store: {total} rows to migrate...") | |
| offset = 0 | |
| migrated = 0 | |
| with pg.cursor() as pgc: | |
| while True: | |
| raw = sq.execute( | |
| "SELECT id, client_id, host, idx, timestamp, version, tag, data, cek, user_id, created_at " | |
| f"FROM store LIMIT {BATCH_SIZE} OFFSET {offset}" | |
| ).fetchall() | |
| if not raw: | |
| break | |
| rows = [ | |
| (to_uuid(r[0]), to_uuid(r[1]), to_uuid(r[2]), r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10]) | |
| for r in raw | |
| ] | |
| psycopg2.extras.execute_values( | |
| pgc, | |
| """ | |
| INSERT INTO store (id, client_id, host, idx, timestamp, version, tag, data, cek, user_id, created_at) | |
| VALUES %s | |
| ON CONFLICT DO NOTHING | |
| """, | |
| rows, | |
| ) | |
| pg.commit() | |
| migrated += len(rows) | |
| offset += BATCH_SIZE | |
| print(f" store: {migrated}/{total}", end="\r", flush=True) | |
| print(f"\nstore: migrated {migrated} row(s)") | |
| def main(): | |
| print(f"Connecting to SQLite: {SQLITE_PATH}") | |
| sq = sqlite3.connect(SQLITE_PATH) | |
| print(f"Connecting to PostgreSQL...") | |
| pg = psycopg2.connect(PG_DSN) | |
| migrate_users(sq, pg) | |
| migrate_sessions(sq, pg) | |
| migrate_store(sq, pg) | |
| sq.close() | |
| pg.close() | |
| print("Done.") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment