# mysql Async **MySQL + MariaDB** wrapper over [aiomysql](https://aiomysql.readthedocs.io/) — a small, config-free, **fail-loud**, two-layer API: friendly portable verbs for the common case, a raw escape hatch for the rest. Third of the datastore trio (`redis` / `psql` / `mysql`), a sibling of the `mongo` lib. Class is **`MysqlDB`**. MySQL and MariaDB are wire-compatible and share the driver, so this covers both. ## Install `requirements.txt`: ``` mysql @ git+ssh://git@git.rethinkstudios.io/rethink-public/mysql.git@v0.1.4 ``` Direct: ```bash pip install "mysql @ git+ssh://git@git.rethinkstudios.io/rethink-public/mysql.git@v0.1.4" ``` Pulls `aiomysql` (which pulls `PyMySQL`). Drop the `@v0.1.3` suffix from the line above to install the latest unpinned. ## The two-layer API **Layer 1 — friendly verbs** for simple single-table CRUD. These hide the dialect and are **signature-identical to the `psql` lib** — same method names, arguments, and return *types* — so you swap psql↔mysql with zero call-site changes. One value-level difference: `insert()` returns the new row id (`lastrowid`) here vs. the inserted rowcount in psql (both `int`); everything else returns the same shape. **Layer 2 — raw escape hatch** for the complex ~20% (joins, aggregates, subqueries). You write the SQL with `%s` placeholders + a params sequence; the wrapper still gives pooling, parameterization, fail-loud errors, and dict rows. Raw SQL is mysql-specific. There is deliberately **nothing in between** — no query builder, no ORM. A join goes through raw `fetch()`, never a chainable `.where()/.join()`. ## Usage ```python from mysql import MysqlDB # construction is sync; connect() builds the pool + SELECT 1 to fail loud on bad config db = await MysqlDB(host="localhost", port=3306, db="app", user="root", password="secret").connect() # --- layer 1: friendly, portable (identical to psql) --- await db.create_table("users", {"id": "int auto_increment primary key", "name": "varchar(255) not null", "age": "int"}) new_id = await db.insert("users", {"name": "ada", "age": 30}) # -> lastrowid rows = await db.get("users", {"age": 30}) # [{"id": 1, "name": "ada", ...}] one = await db.get_one("users", {"name": "ada"}) # {...} or None ok = await db.exists("users", {"name": "ada"}) # True await db.delete("users", {"name": "ada"}) # -> rowcount await db.upsert("users", {"id": 1, "name": "ada2"}, conflict=["id"]) # ON DUPLICATE KEY UPDATE await db.close() ``` Context-manager form: ```python async with MysqlDB(db="app", user="root") as db: await db.insert("events", {"kind": "login"}) ``` ### Layer 2 — raw SQL for the complex queries ```python # a join — not modelled by layer 1, so write it directly. %s placeholders, plain-dict rows. rows = await db.fetch( "SELECT u.name, count(o.id) AS orders " "FROM users u JOIN orders o ON o.user_id = u.id " "WHERE u.active = %s GROUP BY u.name", (True,), ) row = await db.fetchone("SELECT * FROM users WHERE id = %s", (7,)) # dict or None n = await db.fetchval("SELECT count(*) FROM users") # scalar or None await db.execute("UPDATE users SET active = %s WHERE last_seen < %s", (False, cutoff)) # atomic multi-statement block — commits on clean exit, rolls back + re-raises on error async with db.transaction() as conn: async with conn.cursor() as cur: await cur.execute("INSERT INTO ledger(acct, delta) VALUES(%s, %s)", (a, -amount)) await cur.execute("INSERT INTO ledger(acct, delta) VALUES(%s, %s)", (b, +amount)) ``` For anything even Layer 2 doesn't model (`executemany`, server-side cursors), use the raw `db.pool` — the underlying `aiomysql.Pool`. ## Rows & placeholders - **Rows are plain dicts** (`{column: value}`) via aiomysql's `DictCursor` — identical shape to the `psql` lib. - **Values are always parameterized.** Layer 1 builds `%s` internally; Layer 2 takes your `%s` placeholders + a params sequence. Never f-string/`%`-format a value into SQL (the `%s` is the DBAPI placeholder, not Python string formatting). Only identifiers (table/column names) are interpolated, and they're backtick-quoted — Layer 1 also escapes a literal `%` inside an identifier for you. - **A literal `%` in Layer-2 SQL text must be written `%%`.** PyMySQL builds the query via `query % args` whenever you pass params, so `"... LIKE '%foo%'"` with params raises `TypeError`/`ValueError` — write `"... LIKE '%%foo%%'"` instead. Calls with no params are unaffected (no substitution happens), and Layer 1 handles this for you internally. ## Dialect vs psql Layer 1 is portable — you never see these. A Layer-2 raw-SQL author does: | | mysql (this lib) | psql | |---|---|---| | placeholders | `%s` | `$1, $2` | | upsert | `ON DUPLICATE KEY UPDATE` | `ON CONFLICT ... DO UPDATE` | `fetchone` is the unified Layer-2 name in both libs (psql maps it onto asyncpg's `fetchrow` internally; here it's the native DBAPI verb). `upsert()`'s returned rowcount follows MySQL's `ON DUPLICATE KEY UPDATE` convention (1 for an insert, 2 for an update, 0 for a no-op) — a per-row count that differs from psql's; treat it as "affected", not "rows matched". It emits the `col = VALUES(col)` update form, which MySQL 8.0.20+ deprecates (warning 1287, still functional; MariaDB is unaffected). ## Error contract — fail loud Unlike the `mongo` lib (which log-and-swallows), **this lib re-raises.** Every method catches the driver error (`pymysql.err.MySQLError`, `OSError` on connection loss), logs it via `logging.getLogger(__name__)`, and raises. A `None` / `[]` return is only ever a real result (no row, empty table) — never a swallowed failure. ## Surface - **Layer 1:** `create_database`, `create_table(name, {col: "type"})`, `drop(name, table=)`, `insert(table, {col: val})`, `get(table, {conds})`, `get_one(table, {conds})`, `delete(table, {conds})`, `exists(table, {conds})`, `upsert(table, {col: val}, conflict=[...])` - **Layer 2:** `execute(sql, params=None)`, `fetch`, `fetchone`, `fetchval`, `transaction()` - **Raw:** `pool` property → `aiomysql.Pool` `get`/`delete`/`exists`/`get_one` take **equality** conditions only (`col = val AND ...`); anything richer goes through Layer 2. The pool runs with `autocommit=True`; `transaction()` opens an explicit transaction for atomic multi-statement blocks. `cursorclass` is overridable via a constructor kwarg (default `DictCursor`); `fetchval`/`exists` work under either a dict or tuple cursorclass, everything else stays dict-shaped only under the default `DictCursor`. ## Versioning Releases are tagged `vX.Y.Z`. The install line above pins a release; drop the `@vX.Y.Z` suffix to install the latest unpinned. Pin deliberately for reproducible installs.