CI: Migration SQLite to PostgreSQL (#719)

* migrate all extensions
* errors if migration is missing
This commit is contained in:
calle 2022-07-17 13:11:13 +02:00 committed by GitHub
parent f4e7d62ca3
commit ad2aad05e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 167 additions and 31 deletions

View File

@ -4,6 +4,7 @@ docker
docs docs
tests tests
venv venv
tools
*.md *.md
*.log *.log
@ -12,7 +13,6 @@ venv
.gitignore .gitignore
.prettierrc .prettierrc
conv.py
LICENSE LICENSE
Makefile Makefile
mypy.ini mypy.ini

49
.github/workflows/migrations.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: migrations
on: [pull_request]
jobs:
sqlite-to-postgres:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
# maps tcp port 5432 on service container to the host
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
env:
VIRTUAL_ENV: ./venv
PATH: ${{ env.VIRTUAL_ENV }}/bin:${{ env.PATH }}
run: |
python -m venv ${{ env.VIRTUAL_ENV }}
./venv/bin/python -m pip install --upgrade pip
./venv/bin/pip install -r requirements.txt
./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock
- name: Run migrations
run: |
rm -rf ./data
mkdir -p ./data
export LNBITS_DATA_FOLDER="./data"
timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
export LNBITS_DATABASE_URL="postgres://postgres:postgres@0.0.0.0:5432/postgres"
timeout 5s ./venv/bin/uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5001 || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
./venv/bin/python tools/conv.py --dont-ignore-missing

View File

@ -142,8 +142,8 @@ LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# START LNbits # START LNbits
# STOP LNbits # STOP LNbits
# on the LNBits folder, locate and edit 'conv.py' with the relevant credentials # on the LNBits folder, locate and edit 'tools/conv.py' with the relevant credentials
python3 conv.py python3 tools/conv.py
``` ```
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly. Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.

View File

@ -42,7 +42,7 @@ async def m002_redux(db):
""" """
try: try:
for row in [ for row in [
list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlposs") list(row) for row in await db2.fetchall("SELECT * FROM lnurlpos.lnurlpos")
]: ]:
await db.execute( await db.execute(
""" """

View File

@ -120,7 +120,7 @@
<q-card-section> <q-card-section>
<code <code
><span class="text-blue">GET</span> ><span class="text-blue">GET</span>
/lnurldevice/api/v1/lnurlposs</code /lnurldevice/api/v1/lnurlpos</code
> >
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />

View File

@ -1,6 +1,13 @@
import psycopg2 import psycopg2
import sqlite3 import sqlite3
import os import os
import argparse
from environs import Env # type: ignore
env = Env()
env.read_env()
# Python script to migrate an LNbits SQLite DB to Postgres # Python script to migrate an LNbits SQLite DB to Postgres
# All credits to @Fritz446 for the awesome work # All credits to @Fritz446 for the awesome work
@ -11,13 +18,27 @@ import os
# Change these values as needed # Change these values as needed
sqfolder = "data/" sqfolder = "data/"
LNBITS_DATABASE_URL = env.str("LNBITS_DATABASE_URL", default=None)
if LNBITS_DATABASE_URL is None:
pgdb = "lnbits" pgdb = "lnbits"
pguser = "postgres" pguser = "lnbits"
pgpswd = "yourpassword" pgpswd = "postgres"
pghost = "localhost" pghost = "localhost"
pgport = "5432" pgport = "5432"
pgschema = "" pgschema = ""
else:
# parse postgres://lnbits:postgres@localhost:5432/lnbits
pgdb = LNBITS_DATABASE_URL.split("/")[-1]
pguser = LNBITS_DATABASE_URL.split("@")[0].split(":")[-2][2:]
pgpswd = LNBITS_DATABASE_URL.split("@")[0].split(":")[-1]
pghost = LNBITS_DATABASE_URL.split("@")[1].split(":")[0]
pgport = LNBITS_DATABASE_URL.split("@")[1].split(":")[1].split("/")[0]
pgschema = ""
print(pgdb, pguser, pgpswd, pghost, pgport, pgschema)
def get_sqlite_cursor(sqdb) -> sqlite3: def get_sqlite_cursor(sqdb) -> sqlite3:
@ -35,8 +56,6 @@ def get_postgres_cursor():
def check_db_versions(sqdb): def check_db_versions(sqdb):
sqlite = get_sqlite_cursor(sqdb) sqlite = get_sqlite_cursor(sqdb)
dblite = dict(sqlite.execute("SELECT * FROM dbversions;").fetchall()) dblite = dict(sqlite.execute("SELECT * FROM dbversions;").fetchall())
if "lnurlpos" in dblite:
del dblite["lnurlpos"]
sqlite.close() sqlite.close()
postgres = get_postgres_cursor() postgres = get_postgres_cursor()
@ -128,9 +147,14 @@ def migrate_core(sqlite_db_file):
print("Migrated: core") print("Migrated: core")
def migrate_ext(sqlite_db_file, schema): def migrate_ext(sqlite_db_file, schema, ignore_missing=True):
sq = get_sqlite_cursor(sqlite_db_file)
# skip this file it has been moved to ext_lnurldevices.sqlite3
if sqlite_db_file == "data/ext_lnurlpos.sqlite3":
return
print(f"Migrating {sqlite_db_file}.{schema}")
sq = get_sqlite_cursor(sqlite_db_file)
if schema == "bleskomat": if schema == "bleskomat":
# BLESKOMAT LNURLS # BLESKOMAT LNURLS
res = sq.execute("SELECT * FROM bleskomat_lnurls;") res = sq.execute("SELECT * FROM bleskomat_lnurls;")
@ -515,19 +539,19 @@ def migrate_ext(sqlite_db_file, schema):
items = res.fetchall() items = res.fetchall()
insert_to_pg(q, items) insert_to_pg(q, items)
fix_id("offlineshop.items_id_seq", items) fix_id("offlineshop.items_id_seq", items)
elif schema == "lnurlpos": elif schema == "lnurlpos" or schema == "lnurldevice":
# LNURLPOSS # lnurldevice
res = sq.execute("SELECT * FROM lnurlposs;") res = sq.execute("SELECT * FROM lnurldevices;")
q = f""" q = f"""
INSERT INTO lnurlpos.lnurlposs (id, key, title, wallet, currency, timestamp) INSERT INTO lnurldevice.lnurldevices (id, key, title, wallet, currency, device, profit)
VALUES (%s, %s, %s, %s, %s, to_timestamp(%s)); VALUES (%s, %s, %s, %s, %s, %s, %s);
""" """
insert_to_pg(q, res.fetchall()) insert_to_pg(q, res.fetchall())
# LNURLPOS PAYMENT # lnurldevice PAYMENT
res = sq.execute("SELECT * FROM lnurlpospayment;") res = sq.execute("SELECT * FROM lnurldevicepayment;")
q = f""" q = f"""
INSERT INTO lnurlpos.lnurlpospayment (id, posid, payhash, payload, pin, sats, timestamp) INSERT INTO lnurldevice.lnurldevicepayment (id, deviceid, payhash, payload, pin, sats)
VALUES (%s, %s, %s, %s, %s, %s, to_timestamp(%s)); VALUES (%s, %s, %s, %s, %s, %s);
""" """
insert_to_pg(q, res.fetchall()) insert_to_pg(q, res.fetchall())
elif schema == "lnurlp": elif schema == "lnurlp":
@ -546,9 +570,10 @@ def migrate_ext(sqlite_db_file, schema):
success_url, success_url,
currency, currency,
comment_chars, comment_chars,
max max,
fiat_base_multiplier
) )
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s); VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
""" """
pay_links = res.fetchall() pay_links = res.fetchall()
insert_to_pg(q, pay_links) insert_to_pg(q, pay_links)
@ -637,17 +662,79 @@ def migrate_ext(sqlite_db_file, schema):
tracks = res.fetchall() tracks = res.fetchall()
insert_to_pg(q, tracks) insert_to_pg(q, tracks)
fix_id("livestream.tracks_id_seq", tracks) fix_id("livestream.tracks_id_seq", tracks)
elif schema == "lnaddress":
# DOMAINS
res = sq.execute("SELECT * FROM domain;")
q = f"""
INSERT INTO lnaddress.domain(
id, wallet, domain, webhook, cf_token, cf_zone_id, cost, "time")
VALUES (%s, %s, %s, %s, %s, %s, %s, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
# ADDRESSES
res = sq.execute("SELECT * FROM address;")
q = f"""
INSERT INTO lnaddress.address(
id, wallet, domain, email, username, wallet_key, wallet_endpoint, sats, duration, paid, "time")
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s::boolean, to_timestamp(%s));
"""
insert_to_pg(q, res.fetchall())
elif schema == "discordbot":
# USERS
res = sq.execute("SELECT * FROM users;")
q = f"""
INSERT INTO discordbot.users(
id, name, admin, discord_id)
VALUES (%s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
# WALLETS
res = sq.execute("SELECT * FROM wallets;")
q = f"""
INSERT INTO discordbot.wallets(
id, admin, name, "user", adminkey, inkey)
VALUES (%s, %s, %s, %s, %s, %s);
"""
insert_to_pg(q, res.fetchall())
else: else:
print(f"Not implemented: {schema}") print(f"Not implemented: {schema}")
sq.close() sq.close()
if ignore_missing == False:
raise Exception(
f"Not implemented: {schema}. Use --ignore-missing to skip missing extensions."
)
return return
print(f"Migrated: {schema}") print(f"Migrated: {schema}")
sq.close() sq.close()
check_db_versions("data/database.sqlite3") parser = argparse.ArgumentParser(description="Migrate data from SQLite to PostgreSQL")
migrate_core("data/database.sqlite3") parser.add_argument(
dest="sqlite_file",
const=True,
nargs="?",
help="SQLite DB to migrate from",
default="data/database.sqlite3",
type=str,
)
parser.add_argument(
"-i",
"--dont-ignore-missing",
help="Error if migration is missing for an extension.",
required=False,
default=False,
const=True,
nargs="?",
type=bool,
)
args = parser.parse_args()
print(args)
check_db_versions(args.sqlite_file)
migrate_core(args.sqlite_file)
files = os.listdir(sqfolder) files = os.listdir(sqfolder)
for file in files: for file in files:
@ -655,4 +742,4 @@ for file in files:
if file.startswith("ext_"): if file.startswith("ext_"):
schema = file.replace("ext_", "").split(".")[0] schema = file.replace("ext_", "").split(".")[0]
print(f"Migrating: {schema}") print(f"Migrating: {schema}")
migrate_ext(path, schema) migrate_ext(path, schema, ignore_missing=not args.dont_ignore_missing)