2025, Oct 07 11:16

Один PostgreSQL URI в Polars для adbc и connectorx с переключаемым read-only

Как держать один PostgreSQL URI в Polars для чтения и записи: adbc, connectorx, sqlalchemy. Read-only включаем через options, а для connectorx сегмент убираем.

При переносе данных между PostgreSQL и Polars хочется унифицировать одну строку подключения (URI) и использовать её во всех движках. Препятствие — принудительный режим только для чтения через параметр options в URI PostgreSQL. Движок adbc принимает URI с опциями вроде default_transaction_read_only=True, тогда как connectorx не поддерживает дополнительные параметры. Задача — оставить один URI и переключать режим read-only по необходимости, не ведя вручную две версии.

Постановка задачи

Polars умеет читать через pl.read_database_uri с движками connectorx или adbc, а записывать — через DataFrame.write_database с движками adbc или sqlalchemy. Пример PostgreSQL URI с флагом транзакции только для чтения выглядит так:

"postgresql://scott:tiger@localhost:5432/mydatabase?options=-c%20default_transaction_read_only%3DTrue"

Ключевой элемент — default_transaction_read_only=True. Проблема в том, что adbc принимает этот сегмент options, а connectorx не позволяет задавать дополнительные параметры.

Минимальный пример, показывающий несоответствие

import polars as pl

uri_with_ro = (
    r"postgresql://user:pass@localhost:5432/dbname"
    r"?options=-c%20default_transaction_read_only%3DTrue"
)

# Здесь используется один и тот же URI для разных движков.
# adbc принимает сегмент options.
# connectorx не принимает дополнительные параметры в URI.
pl.read_database_uri(
    query="SELECT 1",
    uri=uri_with_ro,
    engine="adbc",
)

# connectorx не принимает дополнительные параметры в URI.
pl.read_database_uri(
    query="SELECT 1",
    uri=uri_with_ro,
    engine="connectorx",
)

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

Поведение движков расходится. adbc работает с URI, содержащим ?options=-c%20default_transaction_read_only%3D..., тогда как connectorx не принимает дополнительные параметры. В итоге единый URI с сегментом options нельзя напрямую использовать в обоих движках без доработки.

Практическое решение: удалять options только для connectorx

Проще всего унифицировать работу так: собрать один канонический URI с переключателем read-only, а затем программно удалять часть ?options только если выбран движок connectorx. Так вся специфическая логика остаётся в одном месте, и режим только для чтения можно включать или выключать единым флагом.

import polars as pl

ro_flag = "False"  # или "True"
dsn_uri = (
    rf"postgresql://username:password@host:port/database"
    rf"?options=-c%20default_transaction_read_only%3D{ro_flag}"
)


def run_query_via_uri(sql: list[str] | str, dsn: str, backend: str, **extras):
    if backend == "connectorx":
        cut = dsn.find("?options", dsn.rfind("/"))
        dsn = dsn[:cut] if cut != -1 else dsn
    return pl.read_database_uri(query=sql, uri=dsn, engine=backend, **extras)


# Унифицированное чтение на разных движках
run_query_via_uri(
    "SELECT * FROM public.a_tbl",
    dsn=dsn_uri,
    backend="adbc",
)
run_query_via_uri(
    "SELECT * FROM public.a_tbl",
    dsn=dsn_uri,
    backend="connectorx",
)

# Унифицированная запись на разных движках
# dataset — это уже существующий DataFrame Polars
dataset.write_database(
    "public.a_tbl",
    connection=dsn_uri,
    engine="adbc",
    if_table_exists="append",
)
dataset.write_database(
    "public.a_tbl",
    connection=dsn_uri,
    engine="sqlalchemy",
    if_table_exists="append",
)

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

Одна строка подключения упрощает интеграционный код и снижает риск расхождений настроек между чтением и записью. В Polars выбор движка для pl.read_database_uri задаётся аргументом engine, а DataFrame.write_database тоже умеет переключать движки. Централизация адаптации строки подключения под конкретный движок удерживает вызывающий код в чистоте и избавляет от разрозненных условных веток по всему проекту.

Итоги

Если вам нужны семантики read-only PostgreSQL прямо в строке подключения, adbc принимает сегмент options, а connectorx дополнительные параметры не поддерживает. Соберите один URI с default_transaction_read_only и удаляйте options только для connectorx. Тогда режим read-only можно переключать одним флагом и использовать одну и ту же строку для чтения через adbc или connectorx и для записи через adbc или sqlalchemy — без жонглирования несколькими шаблонами подключения.

Материал основан на вопросе на StackOverflow от mouwsy и ответе автора mouwsy.