2025, Sep 19 20:02

Транзакции в Polars с ADBC: почему записи видны сразу и как контролировать коммит

Разбираем, почему Polars с engine=adbc коммитит внутри write_database и записи видны сразу. Как добиться транзакций: autocommit не спасает; поможет SQLAlchemy.

Координация нескольких операций записи в одной транзакции — распространённая задача: либо все изменения попадают в базу, либо ни одно. Используя Polars с engine="adbc", логично ожидать, что отключение автокоммита и вызов commit() в конце даст ровно такое поведение. Но на практике записи всё равно становятся видимыми сразу, по одной. Разберёмся, почему так происходит и как работать с этим без сюрпризов.

Минимальный пример

import adbc_driver_postgresql.dbapi as pgx
import polars as pl

link = pgx.connect("postgresql://username:password@host:port/database")

frame = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})

frame.write_database(
    "public.table1",
    connection=link,
    engine="adbc",
)

frame.transpose().write_database(
    "public.table2",
    connection=link,
    engine="adbc",
)

link.commit()

Что происходит на самом деле

Согласно PEP 249 (Python DB-API 2.0), автокоммит по умолчанию должен быть выключен. Драйвер ADBC следует этому правилу. Это видно по сигнатуре класса соединения и при динамической проверке опции: даже если вы ничего не передаёте, автокоммит изначально выключен. Если же вы задаёте переопределение, значением должна быть точная строка "false" (не булево значение), причём с учётом регистра.

import adbc_driver_postgresql.dbapi as pgx
import polars as pl

conn = pgx.connect(
    "postgresql://username:password@host:port/database",
    conn_kwargs={"adbc.connection.autocommit": "false"}
)

print(conn.adbc_connection.get_option("adbc.connection.autocommit"))  # ожидается 'false'

Более того, если вы передадите "true" при подключении, соединение всё равно будет вести себя так, будто автокоммит выключен, пока вы явно не включите его позднее:

conn.adbc_connection.set_autocommit(True)

Здесь и кроется ключевая деталь. Polars выполняет внутренний commit внутри DataFrame.write_database() при engine="adbc". Если автокоммит включён, этот внутренний commit завершается ошибкой, потому что коммитить при уже включённом автокоммите нельзя. По этой ошибке внутренний коммит становится заметен:

adbc_driver_manager.ProgrammingError: INVALID_STATE: 
[libpq] Cannot commit when autocommit is enabled

В публичной документации API это не заявлено, но исходники Polars подтверждают: когда engine равен adbc, write_database() вызывает commit() перед возвратом, и отключить это поведение параметром нельзя.

Корень проблемы

Драйвер базы данных не совершает скрытых коммитов «за вашей спиной». Автокоммит изначально выключен и остаётся выключенным, пока вы не включите его вручную. Немедленная фиксация записей — следствие того, что Polars вызывает commit() внутри write_database() для движка ADBC. Простое отключение автокоммита на соединении не поможет, потому что коммит инициирует сам Polars после каждой записи.

Попытка передать engine_options={"autocommit": False} тоже ничего не изменит. Эти опции пробрасываются в путь загрузки ADBC, который параметр с таким именем не принимает.

Варианты действий

Если вам нужна многооператорная транзакция с полным ручным контролем, есть два пути. Либо менять поведение в самом Polars, убрав внутренний commit в ADBC-пути записи, либо переключиться на движок SQLAlchemy для этой операции. С engine="sqlalchemy" write_database() проходит через pandas.DataFrame.to_sql(), который уважает текущую транзакцию: если вы передаёте подключение SQLAlchemy, которое уже находится в транзакции, библиотека не будет коммитить за вас. Практически это означает, что вы можете выполнить несколько записей, а затем решить, когда зафиксировать изменения или откатить их.

# Концептуальный пример пути через SQLAlchemy (обработка транзакций соблюдается)
# frame.write_database("public.table1", connection=sa_conn, engine="sqlalchemy")
# frame.transpose().write_database("public.table2", connection=sa_conn, engine="sqlalchemy")
# sa_conn.commit()  # коммит произойдёт только по вашему решению

Почему это важно

Атомарность между несколькими таблицами — базовый уровень надёжности для конвейеров данных. Если одна запись прошла, а следующая упала, вы получаете частичное состояние и сложные для отладки несогласованности. Понимание того, где именно происходит commit, избавляет от сюрпризов в продакшене и сохраняет те транзакционные гарантии, на которые вы рассчитывали. Оно также проясняет, что переключение автокоммита — не тот рычаг: поведение задано вызывающей библиотекой, а не драйвером.

Выводы

Если вы хотите вручную контролировать момент, когда партия записей Polars становится видимой, избегайте ADBC-пути, который делает commit внутри. Используйте engine="sqlalchemy" для транзакционного управления, или модифицируйте ADBC-поведение записи в Polars, если вы сопровождаете этот код. Проверяя состояние соединения, помните, что ключ опции ADBC — adbc.connection.autocommit, а значения должны быть точными строками вроде "false". Наконец, не рассчитывайте принудительно выключить автокоммит через engine_options для загрузки ADBC — этот параметр там не распознаётся.

Статья основана на вопросе с StackOverflow от mouwsy и ответе от Zegarek.