groupbwt / scrapy-boilerplate

Scrapy project boilerplate done right
MIT License
42 stars 25 forks source link

[BUGFIX] Include render_postcompile to compile_expression util func #144

Closed LyricalToxic closed 2 months ago

LyricalToxic commented 2 months ago

Bug description

SQLAlchemy во время компайлинга выражений оставляет по умолчанию неотрендеренными postcompile объекты. Для того чтобы их отрендерить необходимо указать явно параметр. doc

Сравнение текущей реализации и фикса.

  def compile_expression_v2(expression: ClauseElement, dialect: Dialect = mysql.dialect()) -> tuple[str, tuple[...]]:
        expression_compiled = expression.compile(dialect=dialect, compile_kwargs={"render_postcompile": True})
        params = tuple(expression_compiled.params.values())
        if position_tup := getattr(expression_compiled, "positiontup", []):
            params = tuple(expression_compiled.params[pos] for pos in position_tup)
        return str(expression_compiled), params

    def compile_expression_v1(expression: ClauseElement, dialect: Dialect = mysql.dialect()) -> tuple[str, tuple[...]]:
        expression_compiled = expression.compile(dialect=dialect)
        return str(expression_compiled), tuple(expression_compiled.params.values())

    stmt = select(Session.id).where(Session.id.in_([TaskStatusCodes.NOT_PROCESSED.value, TaskStatusCodes.ERROR.value]))
    stmt_compiled_v1 = compile_expression_v1(stmt)
    # Compiled v1: ('SELECT sessions.id \nFROM sessions \nWHERE sessions.id IN (__[POSTCOMPILE_id_1])', ([0, 4],))
    print(f"Compiled v1: {stmt_compiled_v1}")
    stmt_compiled_v2 = compile_expression_v2(stmt)
    # Compiled v2: ('SELECT sessions.id \nFROM sessions \nWHERE sessions.id IN (%s, %s)', (0, 4))
    print(f"Compiled v2: {stmt_compiled_v2}")

По комментариям видно, что текущая версия оставляет POSTCOMPILE_id_1 объект неотрендаренным, а вторая версия рендорит и правильно подставляет значения.

Параметры postcompile объектов нарушают порядок всех параметров, поэтому требуется позиционная сортировка

params = tuple(expression_compiled.params[pos] for pos in position_tup)
vulreid commented 2 months ago

c POSTCOMPILE выражением в целом понятно, с этим изменением согласен. что касается

Параметры postcompile объектов нарушают порядок всех параметров, поэтому требуется позиционная сортировка

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

LyricalToxic commented 2 months ago

@vulreid Пример с изменением порядка параметров довольно громоздкий, поэтому не указал его изначально.

Code def compile_expression_v2(expression: ClauseElement, dialect: Dialect = mysql.dialect()) -> tuple[str, tuple[...]]: expression_compiled = expression.compile(dialect=dialect, compile_kwargs={"render_postcompile": True}) params = tuple(expression_compiled.params.values()) if position_tup := getattr(expression_compiled, "positiontup", []): params = tuple(expression_compiled.params[pos] for pos in position_tup) return str(expression_compiled), params def compile_expression_v1_5(expression: ClauseElement, dialect: Dialect = mysql.dialect()) -> tuple[ str, tuple[...]]: expression_compiled = expression.compile(dialect=dialect, compile_kwargs={"render_postcompile": True}) return str(expression_compiled), tuple(expression_compiled.params.values()) def compile_expression_v1(expression: ClauseElement, dialect: Dialect = mysql.dialect()) -> tuple[str, tuple[...]]: expression_compiled = expression.compile(dialect=dialect) return str(expression_compiled), tuple(expression_compiled.params.values()) table_joins = ( DiscoverySessionSearchParameter.__table__.join( Country, Country.id == DiscoverySessionSearchParameter.country_id ) .join(Session, Session.id == DiscoverySessionSearchParameter.session_id) .join(Marketplace, Marketplace.id == DiscoverySessionSearchParameter.marketplace_id) ) stmt = ( select( DiscoverySessionSearchParameter.id, Country.code, ) .select_from(table_joins) .where( and_( Session.status == TaskStatusCodes.IN_QUEUE.value, Marketplace.title == DMTMarketplaces.WALMART.value, DiscoverySessionSearchParameter.status.in_( [ TaskStatusCodes.NOT_PROCESSED.value, TaskStatusCodes.ERROR.value, ] ), DiscoverySessionSearchParameter.attempt < 33, ) ) .limit(100) ) stmt_compiled_v1 = compile_expression_v1(stmt) # Compiled v1: ('SELECT dssp.id, c.code # FROM dssp INNER JOIN c ON c.id = dssp.country_id INNER JOIN sessions ON sessions.id = dssp.session_id INNER JOIN m ON m.id = dssp.marketplace_id # WHERE sessions.status = %s AND m.title = %s AND dssp.status IN (__[POSTCOMPILE_status_2]) AND dssp.attempt < %s # LIMIT %s', (1, 'Walmart', [0, 4], 33, 100)) print(f"Compiled v1: {stmt_compiled_v1}") stmt_compiled_v1_5 = compile_expression_v1_5(stmt) # Compiled v1.5: ('SELECT dssp.id, c.code # FROM dssp INNER JOIN c ON c.id = dssp.country_id INNER JOIN sessions ON sessions.id = dssp.session_id INNER JOIN m ON m.id = dssp.marketplace_id # WHERE sessions.status = %s AND m.title = %s AND dssp.status IN (%s, %s) AND dssp.attempt < %s # LIMIT %s', (1, 'Walmart', 33, 100, 0, 4)) print(f"Compiled v1.5: {stmt_compiled_v1_5}") stmt_compiled_v2 = compile_expression_v2(stmt) # Compiled v2: ('SELECT dssp.id, c.code # FROM dssp INNER JOIN c ON c.id = dssp.country_id INNER JOIN sessions ON sessions.id = dssp.session_id INNER JOIN m ON m.id = dssp.marketplace_id # WHERE sessions.status = %s AND m.title = %s AND dssp.status IN (%s, %s) AND dssp.attempt < %s # LIMIT %s', (1, 'Walmart', 0, 4, 33, 100)) print(f"Compiled v2: {stmt_compiled_v2}")

Необходимо посмотреть непосредственно на подставляемые аргуементы.

  1. V1 - текущая реализация. Порядок (1, 'Walmart', [0, 4], 33, 100) - верный, но постакомпиляция не рендарится
  2. V1.5 - текущая реализация с посткомпиляцией. Порядок (1, 'Walmart', 33, 100, 0, 4) - не верный, Значения для статуса оказались в конце.
  3. V2 - новая реализация. Порядок 1, 'Walmart', 0, 4, 33, 100)) - верный. Восстановлен с помощью сортировки.

Ссылка на доку. Там в блоке есть сортировка

params = (repr(compiled.params[name]) for name in compiled.positiontup)
vulreid commented 2 months ago

@LyricalToxic потестил локально пример, различия в поведении теперь понятны. есть вопрос: по сравнению с примером из документации ты добавил проверку на наличие атрибута, а разбирался ли ты откуда в экземпляре expression_compiled = expression.compile() этот атрибут появляется (в какой момент, за счет какой иерархии наследования\инициализации)?

LyricalToxic commented 2 months ago

@vulreid Нет, не разбирался, не исследовал. Стоит этим заняться?

vulreid commented 2 months ago

нет, если нет готовой информации, то сейчас не надо искать, само поведение уже подтверждено.