Обернуть наименования заголовков в двойные кавычки при использовании COPY TO CSV

Через Python сохраняю таблицу из Postgresql в CSV с помощью copy_expert:

query = f'''copy ({function}) to stdout with (format csv, delimiter ',', header true, force_quote *);'''

with db_connection.cursor() as cursor, open(file_path, "w") as file:
    cursor.copy_expert(query, file)

Опция FORCE_QUOTE оборачивает в двойные кавычки данные, но не имена заголовков, которые сейчас выгружаются таким образом:

ID,IPV,MAC_ADDR, ...

Мне же требуется заголовки выгружать внутри "":

"ID","IPV","MAC_ADDR", ...

Изначально в select оборачивание в двойные кавычки используется как способ корректно указать псевдонимы - они не идут дальше как часть имени:

select
    (v.dtl_id ||'_'|| v.id)::varchar as "ID",
    null:varchar as "IPV",
    и т.д. ...

Попробовал экранировать двойные кавычки с помощью ещё одних:

select
    (v.dtl_id ||'_'|| v.id)::varchar as """ID""",
    null:varchar as """IPV""",
    и т.д. ...

В том же DBeaver это работает корректно - при запросе имя колонки начинает отображаться внутри двойных кавычек. Но при выгрузке CSV вышеуказанным методом почему-то заголовки сохраняются с 3-мя парами двойных кавычек - как будто исходное экранирование пропадает и считывается как есть:

"""ID""","""IPV""","""MAC_ADDR""", ...

Есть ли иной способ экранировать двойные кавычки для псевдонимов, или иная опция для их оборачивания как FORCE_QUOTE для данных?

UPD: нашел другой способ в Postgresql указать двойные кавычки:

select
    (v.dtl_id ||'_'|| v.id)::varchar as U&"\0022IDU&"\0022,
    null:varchar as U&"\0022IPVU&"\0022,
    и т.д. ...

Однако, он опять таки работает внутри SQL-инструментов, но при формировании CSV на выходе тройные двойные кавычки.

В качестве теста ещё проверил pandas - там тоже заголовки оказываются по итогу в тройных двойных кавычках:

import pandas as pd

d = {'1': [1, 2, 3], '"2"': ['x', 'ooo "abc"', 'y']}
df = pd.DataFrame.from_dict(d)
df.to_csv(
    'test.csv',
    sep=',',
    header=True,
    index=False
)

На выходе CSV:

1,"""2"""
1,x
2,"ooo ""abc"""
3,y

Будто и в COPY, и в pandas механизмы экранирования кавычек и оборачивания данных в кавычки наслаиваются друг на друга, что в результате не дает просто указать заголовки только в одних двойных кавычках.

Предположение следующее:

"  "  "  ID"""
      ^-- исходные двойные кавычки
   ^-- кавычки для экранирования исходных
^-- кавычки от оборачивания данных (почему то распространилось и на заголовок)

Ответы (1 шт):

Автор решения: mrgervant

По итогам тестов и изысканий нашелся способ не через COPY TO, а через pandas. По всей видимости непосредственно в COPY TO мало опций - для того же COPY FROM имеется больше, как FORCE_NOT_NULL и т.п.

При использовании pd.to_csv() можно указать параметр quoting=csv.QUOTE_ALL - он позволяет обернуть в кавычки как все данные (аналогично FORCE_QUOTE), так и заголовки (документация). В модуле csv имеются и другие константы, благодаря которым можно настроить формирование файла гибче, чем в случае с COPY TO + copy_expert.

В моём случае перегоняются большие объемы данных (до 10Гб), поэтому просто в датафрейм загрузить всю информацию из БД не выйдет - необходимо экспортировать данные из PostgreSQL в CSV по кускам (чанкам):

import os
import csv

import pandas as pd
from sqlalchemy import create_engine, Engine


def upload_to_csv(db_engine: Engine, file_name: str, query: str, chunk: int) -> None:
    db_connection = db_engine.connect().execution_options(
        stream_results=True,
        max_row_buffer=chunk
    )
    logger.info(f"Загрузка CSV {file_name}")
    try:
        # в первый df пишем заголовки и данные
        header, mode = True, 'w'
        for df in pd.read_sql(query, db_connection, chunksize=chunk):
            df.to_csv(file_name, mode=mode, header=header, index=False, quoting=csv.QUOTE_ALL)
            # в следующие df заголовки уже пропускаем, т.к. теперь добавляем оставшиеся строки
            if header:
                header, mode = False, 'a'
        logger.info("Загрузка успешно завершена")
    except Exception as e:
        logger.error(f"Ошибка загрузки CSV {file_name}: {e}")
    finally:
        db_connection.close()


# Требуемые аргументы для функции upload_to_csv()
db_engine = create_engine(conn_string)  # conn_string - строка с параметрами подключения к БД
file_name = os.path.join('my_path', 'test.csv')
query = '''SELECT var1, var2 ... FROM schema.table WHERE ...'''
chunk = 10000

В результате получаем требуемый CSV:

"ID","IPV","MAC_ADDR", ...
"1234567890","192.168.0.1","00-00-00-00-00-00", ...

P.S.: вероятно есть какой-то вариант объединить базовый csv и copy_expert, чтобы без pandas (с его нюансами по приведению типов данных), но мне собрать такую функцию не удалось. Возможный ответ с подобным решением приветствуется.

→ Ссылка