Polars の write_database() を INSERT OR IGNORE にする
polars.DataFrame.write_database で DataFrame を DB に書き込めるが、内部で実行されるのは INSERT 文なので既存テーブルに追加する際などにこれを INSERT OR IGNORE にしたくなることがある。これを実現する方法をメモ。
環境
DB は SQLite、write_database() は engine="sqlalchemy" を想定。
polars==0.18.15, pandas==1.5.3 で確認。
背景
PRIMARY KEY 制約で重複が禁止されているテーブルに重複した値を持つ行を追加しようとするとエラーになる。
このような場合に「重複しているものは無視して追加」を実現する SQL 文として INSERT OR IGNORE がある。
これを polars の write_database() でも実現したい。
import sqlite3 import polars as pl DB_NAME="tmp.db" TABLE_NAME="test_tbl" conn = sqlite3.connect(DB_NAME) cur = conn.cursor() # `id` の重複を許さないテーブルを作成 cur.execute(f"CREATE TABLE {TABLE_NAME} (id INT, value STR, PRIMARY KEY(id))") # データ挿入 cur.execute(f"INSERT INTO {TABLE_NAME} (id, abc) VALUES(111, 'old')") # 追加データを挿入、`id=111` が重複しているのでエラー # sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: test_tbl.id df = pl.DataFrame({"id": [111, 222], "value": ["new", "abc"]}) df.write_database(TABLE_NAME, f"sqlite:///{DB_NAME}", if_exists="append")
方法
pandas.io.sql.SQLTable._execute_insert を置き換える:
from pandas.io.sql import SQLTable def _execute_insert_or_ignore(self, conn, keys: list[str], data_iter) -> int: data = [dict(zip(keys, row)) for row in data_iter] result = conn.execute(self.table.insert().prefix_with("OR IGNORE"), data) return result.rowcount SQLTable._execute_insert = _execute_insert_or_ignore
変更点は self.table.insert() を self.table.insert().prefix_with("OR IGNORE") にしたのみ1。
こうすると前述のコード実行後テーブルは以下のようになる( id=111 の値(old)は変わらずに id=222 が挿入される)。
┌─────┬───────┐ │ id ┆ value │ │ --- ┆ --- │ │ i64 ┆ str │ ╞═════╪═══════╡ │ 111 ┆ old │ │ 222 ┆ abc │ └─────┴───────┘
原理としては、 write_database(engine="sqlalchemy") は内部で to_pandas() して pandas.DataFrame.to_sql を実行している(該当部分)。
なので to_sql() の内部で SQLAlchemy を実行する部分を置き換えている。
代替案
上記の方法だとすべての write_table() が INSERT OR IGNORE になってしまう。もちろん
orig = SQLTable._execute_insert SQLTable._execute_insert = _execute_insert_or_ignore df.write_table() SQLTable._execute_insert = orig
のような泥臭い方法も考えられるが面倒。
正攻法としては write_table() の引数に method を追加して to_sql()にこの method を渡すように修正、実行時に method=_execute_insert_or_ignore とすることで IGNORE するかどうか切り替える、みたいな感じかなと思うがengine=adbc のときとの整合性とかが面倒かもしれない。
別の方法としては stack overflow にあるように一旦(新規の)別テーブルに write_database() してこのテーブルから目的のテーブルに INSERT OR IGNORE するというのがあるらしい。
INSERT が2回走ることになるけど場合によってはこれもアリかもしれない。
-
MySQL なら
prefix_with("IGNORE")にすればよいらしい( stack overflow)↩