Files
databisnisid/db.py
T
proitlab fdb5d0de15 T17: backend penyimpanan bersama db.py (sqlite | mysql via PyMySQL)
- db.py: abstraksi connect/query/queryone/replace_snapshot; %s → ? utk sqlite;
  replace snapshot mysql = CREATE IF NOT EXISTS + DELETE + INSERT (MVCC)
- config/.env.example: VEX_DB_BACKEND (default sqlite) + VEX_DB_HOST/PORT/USER/PASS/NAME
- get_voters.persist → db.replace_snapshot; dashboard baca via db.query
- LIKE pakai ESCAPE '!' (backslash memutus literal string MySQL, B2);
  db.query selalu kembalikan list (PyMySQL tuple vs sqlite list, B3)
- docker-compose.yml: kontainer uji mariadb:11 (databisnisid-mariadb, 127.0.0.1:3306)
- test_mariadb.py: suite integrasi opt-in (skip bila bukan mysql)
- SPEC.md §V17/T17/B2/B3; AGENTS.md runbook
2026-08-05 12:41:17 +07:00

98 lines
3.1 KiB
Python

"""Abstraksi penyimpanan: sqlite (default) atau MariaDB/MySQL (PyMySQL).
Pilih backend via config `VEX_DB_BACKEND` (env `.env`). Kedua backend memakai
skema `voters` yang sama; query berbagi sintaks ditulis dengan placeholder
`%s` yang diterjemahkan ke `?` untuk sqlite. Dashboard read-only (V10).
"""
import sqlite3
from config import (DB_BACKEND, DB_HOST, DB_NAME, DB_PASS, DB_PORT, DB_PATH,
DB_USER)
# Skema voters — sama untuk kedua backend (I.db)
_COLS = (
'owner VARCHAR(13) PRIMARY KEY, '
'weight TEXT NOT NULL, '
'staked DOUBLE NOT NULL, '
'scanned_at TEXT NOT NULL, '
'last_vote TEXT'
)
_SCHEMA = f'CREATE TABLE voters ({_COLS})'
_SCHEMA_IF_NOT_EXISTS = f'CREATE TABLE IF NOT EXISTS voters ({_COLS})'
def _translate(sql):
"""sqlite pakai `?`; mysql pakai `%s`. Ubah `%s` → `?` untuk sqlite."""
return sql.replace('%s', '?') if DB_BACKEND == 'sqlite' else sql
def connect():
"""Buka koneksi backend. sqlite: WAL agar pembaca tak terblokir."""
if DB_BACKEND == 'sqlite':
conn = sqlite3.connect(DB_PATH)
conn.execute('PRAGMA journal_mode=WAL')
return conn
if DB_BACKEND == 'mysql':
import pymysql
return pymysql.connect(
host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASS,
database=DB_NAME, charset='utf8mb4',
)
raise ValueError(f'VEX_DB_BACKEND tak dikenal: {DB_BACKEND!r}')
def query(sql, params=()):
"""Jalankan SELECT → daftar baris (tuple), koneksi dibuka-tutup tiap panggil."""
conn = connect()
try:
cur = conn.cursor()
cur.execute(_translate(sql), params)
return list(cur.fetchall()) # sqlite→list, PyMySQL→tuple: seragamkan
finally:
conn.close()
def queryone(sql, params=()):
"""Jalankan SELECT → baris pertama (tuple) atau None."""
conn = connect()
try:
cur = conn.cursor()
cur.execute(_translate(sql), params)
return cur.fetchone()
finally:
conn.close()
def replace_snapshot(rows, scanned_at):
"""V7: ganti snapshot voters tiap skan (⊥ history/append).
sqlite: DROP+CREATE+INSERT (WAL melindungi pembaca).
mysql: CREATE IF NOT EXISTS + DELETE + INSERT dalam satu transaksi
→ MVCC memberi pembaca snapshot konsisten (⊥ torn read saat ganti harian).
"""
tuples = [(r['owner'], r['weight'], r['staked'], scanned_at, r.get('last_vote'))
for r in rows]
conn = connect()
try:
cur = conn.cursor()
if DB_BACKEND == 'sqlite':
cur.execute('DROP TABLE IF EXISTS voters')
cur.execute(_SCHEMA)
cur.executemany(
'INSERT INTO voters (owner, weight, staked, scanned_at, last_vote) '
'VALUES (?, ?, ?, ?, ?)',
tuples,
)
else:
cur.execute(_SCHEMA_IF_NOT_EXISTS)
cur.execute('DELETE FROM voters')
cur.executemany(
'INSERT INTO voters (owner, weight, staked, scanned_at, last_vote) '
'VALUES (%s, %s, %s, %s, %s)',
tuples,
)
conn.commit()
finally:
conn.close()