Clione-SQL を知っていますか?
Java で DB アクセスのコードを書いたことがある人なら、検索条件の動的組み立てで苦しんだ経験があると思います。MyBatis の <if> タグ地獄、JPA の Criteria API の可読性崩壊、あるいは StringBuilder で SQL を組み立てる原始的な戦い。
そんな中で Clione-SQL というライブラリが提案したアプローチは、かなり異質でした。
SELECT * FROM employee
WHERE
dept_id = /* $dept_id */1
AND name = /* $name */'山田'
AND status = /* $status */'active'
これがそのまま SQL テンプレートです。SQL コメントの中にパラメータ名を書いて、直後にデフォルト値を置く。DB ツールでそのまま実行すればデフォルト値で動くし、アプリケーション経由ならパラメータがバインドされる。いわゆる 2way SQL です。
かゆいところに手が届く行削除の仕様
Clione-SQL の何がすごいかというと、動的な条件組み立てのルールがシンプルなのに表現力が高いところです。
$ 付きパラメータに None を渡すと、その行が丸ごと消える。たったこれだけのルールで、動的検索が実現できます。
# name を指定しない → name の行が消える
db.query(User, "search.sql", {"dept_id": 10, "name": None, "status": "active"})
-- name の行が消えて、先頭に残った AND も自動除去される
SELECT * FROM employee
WHERE
dept_id = ?
AND status = ?
さらに、インデントで親子関係が定義されるので、括弧でグルーピングした条件も自然に扱えます。
WHERE
id = /* $id */1
AND (
status = /* $status1 */'active'
OR status = /* $status2 */'pending'
)
status1 と status2 が両方 None なら、括弧ブロックごと消える。子が全部消えたら親も消える。WHERE の下の条件が全部消えたら WHERE 自体も消える。if 文を一つも書かずに、です。
IN 句もリストを渡せば自動展開されるし、空リストなら IN (NULL) になって 0 件。このへんの「あー、そうそう、そこ面倒なんだよ」というポイントを的確に潰してくれる設計が Clione-SQL の魅力だと思っています。
それを Python でも使いたかった
普段 Python でちょっとしたツールやバッチ処理を書くことが増えて、そのたびに「Clione-SQL があればなあ」と思っていました。Python の DB アクセスは ORM(SQLAlchemy 等)か生 SQL かの二択で、この「SQL はそのまま書きたいけど条件組み立ては楽したい」という中間地帯をカバーするものがない。
ということで、Clione-SQL と Doma2 の設計を Python にポーティングしたのが sqlym です。
pip install sqlym
使い方
dataclass を定義して、SQL ファイルを書いて、Sqlym 経由で実行するだけです。
from dataclasses import dataclass
from sqlym import Sqlym
@dataclass
class User:
id: int | None = None
name: str = ""
email: str = ""
conn = sqlite3.connect("example.db")
conn.row_factory = sqlite3.Row
db = Sqlym(conn, sql_dir="sql")
SQL ファイル — DB ツールでそのまま実行できる
-- sql/select_by_name.sql
SELECT id, name, email
FROM users
WHERE 1 = 1
AND name = /* $name */'John'
ORDER BY id
Python から呼ぶ
# 条件あり
users = db.query(User, "select_by_name.sql", {"name": "Alice"})
# 条件なし(name の行が消える)
users = db.query(User, "select_by_name.sql", {"name": None})
INSERT / UPDATE / DELETE も同じ要領です。
db.execute("insert.sql", {"name": "Alice", "email": "alice@example.com"})
user_id = db.insert("insert.sql", {"name": "Bob", "email": "bob@example.com"}) # lastrowid が返る
affected = db.execute("update.sql", {"id": 1, "name": "New Name", "email": "new@example.com"})
affected = db.execute("delete.sql", {"id": 3})
SQLite, PostgreSQL, MySQL, Oracle に対応していて、プレースホルダの形式や LIKE エスケープの違い、Oracle の IN 句 1000 件制限なども吸収します。仕様の詳細は SQL_SYNTAX.ja.md をご覧ください。
リポジトリの examples/ に基本的な CRUD、バッチ処理、トランザクション管理、Clean Architecture での構成例を置いてあるので、気になる方はそちらもどうぞ。
AI コーディングエージェントの時代に思うこと
この sqlym、コーディングの大部分を AI エージェントと一緒に進めました。
自分がやったのは「Clione-SQL のこの仕様を Python で再現したい」という設計意図を伝えることと、出来上がったものを触って「ここ違う」「こうじゃない」とフィードバックすること。いわゆるアーキテクチャとレビューの部分です。
ひと昔前なら、仕事の合間に別言語へのポーティングをやろうなんて思えなかった。Python の DB ドライバごとのプレースホルダの差異を調べて、IN 句の展開処理を書いて、テストを揃えて……正直、モチベーションが続かなかったと思います。
それが今は「こういうものが欲しい」をきちんと言語化できれば、形にできるようになった。AI エージェントは言われたとおりにコードを書くのは得意だけど、「何を作るか」「なぜこの設計なのか」は人間が決める必要がある。その意味で、プログラマの仕事が「コードを書くこと」から「設計と意思決定」にシフトしていくのを実感した経験でもありました。
「作りたいものはあるけど、手を動かす時間がない」という人にとって、今はかなりいい時代だと思います。
おわりに
繰り返しになりますが、sqlym がやっていることの本質は Clione-SQL と Doma2 が確立した設計の Python 移植です。2way SQL という発想、行単位の処理、インデントによる親子関係、パラメータコメント構文——これらはすべて先人の知恵をお借りしています。
Python で「SQL をそのまま書きたいけど動的条件の組み立てが面倒」と感じている方は、ぜひ試してみてください。
フィードバックや Issue お待ちしています。