SQLAlchemy: imprime la consulta real

165

Realmente me gustaría poder imprimir SQL válido para mi aplicación, incluidos los valores, en lugar de los parámetros de enlace, pero no es obvio cómo hacerlo en SQLAlchemy (por diseño, estoy bastante seguro).

¿Alguien ha resuelto este problema de manera general?

bukzor
fuente
1
No lo he hecho, pero probablemente podrías construir una solución menos frágil tocando en el sqlalchemy.engineregistro de SQLAlchemy . Registra consultas y parámetros de enlace, solo tendría que reemplazar los marcadores de posición de enlace con los valores en una cadena de consulta SQL fácilmente construida.
Simon
@Simon: hay dos problemas con el uso del registrador: 1) solo se imprime cuando se ejecuta una instrucción 2) Todavía tendría que hacer un reemplazo de cadena, excepto en ese caso, no sabría exactamente la cadena de plantilla de enlace , y tendría que analizarlo de alguna manera fuera del texto de la consulta, haciendo que la solución sea más frágil.
bukzor
La nueva URL parece ser docs.sqlalchemy.org/en/latest/faq/… para las preguntas frecuentes de @ zzzeek.
Jim DeLaHunt

Respuestas:

167

En la gran mayoría de los casos, la "cadena" de una declaración o consulta SQLAlchemy es tan simple como:

print str(statement)

Esto se aplica tanto a un ORM Querycomo a cualquier select()otra declaración.

Nota : la siguiente respuesta detallada se mantiene en la documentación de sqlalchemy .

Para obtener la declaración como compilada en un dialecto o motor específico, si la declaración en sí no está vinculada a una, puede pasar esto a compile () :

print statement.compile(someengine)

o sin motor:

from sqlalchemy.dialects import postgresql
print statement.compile(dialect=postgresql.dialect())

Cuando se le da un Queryobjeto ORM , para obtener el compile()método solo necesitamos acceder primero al descriptor de acceso de declaración :

statement = query.statement
print statement.compile(someengine)

con respecto a la estipulación original de que los parámetros vinculados deben "insertarse" en la cadena final, el desafío aquí es que SQLAlchemy normalmente no tiene la tarea de esto, ya que Python DBAPI lo maneja adecuadamente, sin mencionar que omitir los parámetros vinculados es probablemente los agujeros de seguridad más ampliamente explotados en las aplicaciones web modernas. SQLAlchemy tiene una capacidad limitada para hacer esta cadena en ciertas circunstancias, como la de emitir DDL. Para acceder a esta funcionalidad, se puede usar el indicador 'literal_binds', que se pasa a compile_kwargs:

from sqlalchemy.sql import table, column, select

t = table('t', column('x'))

s = select([t]).where(t.c.x == 5)

print s.compile(compile_kwargs={"literal_binds": True})

el enfoque anterior tiene la advertencia de que solo es compatible con tipos básicos, como ints y cadenas, y además, si bindparam se usa directamente un valor sin un valor preestablecido, tampoco podrá encadenar eso.

Para admitir la representación literal en línea para los tipos no admitidos, implemente un TypeDecoratorpara el tipo de destino que incluye un TypeDecorator.process_literal_parammétodo:

from sqlalchemy import TypeDecorator, Integer


class MyFancyType(TypeDecorator):
    impl = Integer

    def process_literal_param(self, value, dialect):
        return "my_fancy_formatting(%s)" % value

from sqlalchemy import Table, Column, MetaData

tab = Table('mytable', MetaData(), Column('x', MyFancyType()))

print(
    tab.select().where(tab.c.x > 5).compile(
        compile_kwargs={"literal_binds": True})
)

produciendo resultados como:

SELECT mytable.x
FROM mytable
WHERE mytable.x > my_fancy_formatting(5)
zzzeek
fuente
2
Esto no pone comillas alrededor de las cadenas y no resuelve algunos parámetros vinculados.
bukzor
1
La segunda mitad de la respuesta se ha actualizado con la información más reciente.
zzzeek
2
@zzzeek ¿Por qué las consultas de impresión bonita no se incluyen en sqlalchemy de forma predeterminada? Al igual query.prettyprint(). Alivia enormemente el dolor de depuración con grandes consultas.
jmagnusson
2
@jmagnusson porque la belleza está en el ojo del espectador :) Hay muchos ganchos (por ejemplo, evento cursor_execute, filtros de registro de Python @compiles, etc.) para cualquier cantidad de paquetes de terceros para implementar sistemas de impresión bonita.
zzzeek
1
@buzkor re: límite que se ha corregido en 1.0 bitbucket.org/zzzeek/sqlalchemy/issue/3034/…
zzzeek
66

Esto funciona en python 2 y 3 y es un poco más limpio que antes, pero requiere SA> = 1.0.

from sqlalchemy.engine.default import DefaultDialect
from sqlalchemy.sql.sqltypes import String, DateTime, NullType

# python2/3 compatible.
PY3 = str is not bytes
text = str if PY3 else unicode
int_type = int if PY3 else (int, long)
str_type = str if PY3 else (str, unicode)


class StringLiteral(String):
    """Teach SA how to literalize various things."""
    def literal_processor(self, dialect):
        super_processor = super(StringLiteral, self).literal_processor(dialect)

        def process(value):
            if isinstance(value, int_type):
                return text(value)
            if not isinstance(value, str_type):
                value = text(value)
            result = super_processor(value)
            if isinstance(result, bytes):
                result = result.decode(dialect.encoding)
            return result
        return process


class LiteralDialect(DefaultDialect):
    colspecs = {
        # prevent various encoding explosions
        String: StringLiteral,
        # teach SA about how to literalize a datetime
        DateTime: StringLiteral,
        # don't format py2 long integers to NULL
        NullType: StringLiteral,
    }


def literalquery(statement):
    """NOTE: This is entirely insecure. DO NOT execute the resulting strings."""
    import sqlalchemy.orm
    if isinstance(statement, sqlalchemy.orm.Query):
        statement = statement.statement
    return statement.compile(
        dialect=LiteralDialect(),
        compile_kwargs={'literal_binds': True},
    ).string

Manifestación:

# coding: UTF-8
from datetime import datetime
from decimal import Decimal

from literalquery import literalquery


def test():
    from sqlalchemy.sql import table, column, select

    mytable = table('mytable', column('mycol'))
    values = (
        5,
        u'snowman: ☃',
        b'UTF-8 snowman: \xe2\x98\x83',
        datetime.now(),
        Decimal('3.14159'),
        10 ** 20,  # a long integer
    )

    statement = select([mytable]).where(mytable.c.mycol.in_(values)).limit(1)
    print(literalquery(statement))


if __name__ == '__main__':
    test()

Da este resultado: (probado en python 2.7 y 3.4)

SELECT mytable.mycol
FROM mytable
WHERE mytable.mycol IN (5, 'snowman: ☃', 'UTF-8 snowman: ☃',
      '2015-06-24 18:09:29.042517', 3.14159, 100000000000000000000)
 LIMIT 1
bukzor
fuente
2
Esto es increíble ... Tendremos que agregar esto a algunas librerías de depuración para que podamos acceder fácilmente a él. Gracias por hacer el trabajo de pies en este caso. Me sorprende que haya sido tan complicado.
Corey O.
55
Estoy bastante seguro de que esto es intencionalmente difícil, porque los novatos están tentados a cursor.execute () esa cadena. Sin embargo, el principio de consentimiento de los adultos se usa comúnmente en Python.
bukzor
Muy útil. ¡Gracias!
clima
Muy bueno de verdad. Me tomé la libertad e incorporé esto en stackoverflow.com/a/42066590/2127439 , que cubre SQLAlchemy v0.7.9 - v1.1.15, incluidas las declaraciones INSERT y UPDATE (PY2 / PY3).
wolfmanx
muy agradable. pero se está convirtiendo de la siguiente manera. 1) query (Table) .filter (Table.Column1.is_ (False) to WHERE Column1 IS 0. 2) query (Table) .filter (Table.Column1.is_ (True) to WHERE Column1 IS 1. 3) query ( Table) .filter (Table.Column1 == func.any ([1,2,3])) a WHERE Column1 = any ('[1,2,3]') las conversiones anteriores son incorrectas en sintaxis.
Sekhar C
51

Dado que lo que desea tiene sentido solo al depurar, puede iniciar SQLAlchemy con echo=True, para registrar todas las consultas SQL. Por ejemplo:

engine = create_engine(
    "mysql://scott:tiger@hostname/dbname",
    encoding="latin1",
    echo=True,
)

Esto también se puede modificar para una sola solicitud:

echo=False- si True, el motor registrará todas las declaraciones, así como una repr()de sus listas de parámetros en el registrador de motores, que por defecto es sys.stdout. El echoatributo de Enginese puede modificar en cualquier momento para activar y desactivar el inicio de sesión. Si se establece en la cadena "debug", las filas de resultados también se imprimirán en la salida estándar. Esta bandera finalmente controla un registrador de Python; consulte Configuración de registro para obtener información sobre cómo configurar el registro directamente.

Fuente: Configuración del motor SQLAlchemy

Si se usa con Flask, simplemente puede configurar

app.config["SQLALCHEMY_ECHO"] = True

para obtener el mismo comportamiento.

Vedran Šego
fuente
66
Esta respuesta merece ser mucho más alta ... y para los usuarios de flask-sqlalchemyesta debería ser la respuesta aceptada.
jso
25

Podemos usar el método de compilación para este propósito. De los documentos :

from sqlalchemy.sql import text
from sqlalchemy.dialects import postgresql

stmt = text("SELECT * FROM users WHERE users.name BETWEEN :x AND :y")
stmt = stmt.bindparams(x="m", y="z")

print(stmt.compile(dialect=postgresql.dialect(),compile_kwargs={"literal_binds": True}))

Resultado:

SELECT * FROM users WHERE users.name BETWEEN 'm' AND 'z'

Advertencia de documentos:

Nunca use esta técnica con contenido de cadenas recibido de entradas no confiables, como formularios web u otras aplicaciones de entrada de usuario. Las instalaciones de SQLAlchemy para coaccionar los valores de Python en valores de cadena SQL directos no son seguros contra la entrada no confiable y no validan el tipo de datos que se pasan. Utilice siempre parámetros enlazados cuando invoque mediante programación sentencias SQL no DDL contra una base de datos relacional.

akshaynagpal
fuente
13

Entonces, basándose en los comentarios de @ zzzeek sobre el código de @ bukzor, se me ocurrió esto para obtener fácilmente una consulta "bastante imprimible":

def prettyprintable(statement, dialect=None, reindent=True):
    """Generate an SQL expression string with bound parameters rendered inline
    for the given SQLAlchemy statement. The function can also receive a
    `sqlalchemy.orm.Query` object instead of statement.
    can 

    WARNING: Should only be used for debugging. Inlining parameters is not
             safe when handling user created data.
    """
    import sqlparse
    import sqlalchemy.orm
    if isinstance(statement, sqlalchemy.orm.Query):
        if dialect is None:
            dialect = statement.session.get_bind().dialect
        statement = statement.statement
    compiled = statement.compile(dialect=dialect,
                                 compile_kwargs={'literal_binds': True})
    return sqlparse.format(str(compiled), reindent=reindent)

Personalmente, me resulta difícil leer el código que no está sangrado, así que solía sqlparsereindentificar el SQL. Se puede instalar con pip install sqlparse.

jmagnusson
fuente
@bukzor Todos los valores funcionan excepto el datatime.now()que usa Python 3 + sqlalchemy 1.0. Tendría que seguir los consejos de @zzzeek para crear un TypeDecorator personalizado para que funcione también.
jmagnusson
Eso es un poco demasiado específico. La fecha y hora no funciona en ninguna combinación de python y sqlalchemy. Además, en py27, el unicode no ascii causa una explosión.
bukzor
Por lo que pude ver, la ruta de TypeDecorator requiere que modifique las definiciones de mi tabla, lo cual no es un requisito razonable para ver mis consultas. Edité mi respuesta para estar un poco más cerca de la suya y la de zzzeek, ​​pero tomé la ruta de un dialecto personalizado, que es propiamente ortogonal a las definiciones de la tabla.
bukzor
11

Este código se basa en la brillante respuesta existente de @bukzor. Acabo de agregar render personalizado para datetime.datetimeescribir en Oracle TO_DATE().

Siéntase libre de actualizar el código para adaptarlo a su base de datos:

import decimal
import datetime

def printquery(statement, bind=None):
    """
    print a query, with values filled in
    for debugging purposes *only*
    for security, you should always separate queries from their values
    please also note that this function is quite slow
    """
    import sqlalchemy.orm
    if isinstance(statement, sqlalchemy.orm.Query):
        if bind is None:
            bind = statement.session.get_bind(
                    statement._mapper_zero_or_none()
            )
        statement = statement.statement
    elif bind is None:
        bind = statement.bind 

    dialect = bind.dialect
    compiler = statement._compiler(dialect)
    class LiteralCompiler(compiler.__class__):
        def visit_bindparam(
                self, bindparam, within_columns_clause=False, 
                literal_binds=False, **kwargs
        ):
            return super(LiteralCompiler, self).render_literal_bindparam(
                    bindparam, within_columns_clause=within_columns_clause,
                    literal_binds=literal_binds, **kwargs
            )
        def render_literal_value(self, value, type_):
            """Render the value of a bind parameter as a quoted literal.

            This is used for statement sections that do not accept bind paramters
            on the target driver/database.

            This should be implemented by subclasses using the quoting services
            of the DBAPI.

            """
            if isinstance(value, basestring):
                value = value.replace("'", "''")
                return "'%s'" % value
            elif value is None:
                return "NULL"
            elif isinstance(value, (float, int, long)):
                return repr(value)
            elif isinstance(value, decimal.Decimal):
                return str(value)
            elif isinstance(value, datetime.datetime):
                return "TO_DATE('%s','YYYY-MM-DD HH24:MI:SS')" % value.strftime("%Y-%m-%d %H:%M:%S")

            else:
                raise NotImplementedError(
                            "Don't know how to literal-quote value %r" % value)            

    compiler = LiteralCompiler(dialect, statement)
    print compiler.process(statement)
vvladymyrov
fuente
22
No veo por qué la gente de SA cree que es razonable que una operación tan simple sea tan difícil .
bukzor
¡Gracias! render_literal_value funcionó bien para mí. Mi único cambio fue: en return "%s" % valuelugar de return repr(value)en la sección float, int, long porque Python estaba produciendo longs como en 22Llugar de solo22
OrganicPanda
Esta receta (así como la original) genera UnicodeDecodeError si algún valor de cadena de bindparam no es representable en ascii. Publiqué una esencia que corrige esto.
gsakkis
1
"STR_TO_DATE('%s','%%Y-%%m-%%d %%H:%%M:%%S')" % value.strftime("%Y-%m-%d %H:%M:%S")en mysql
Zitrax
1
@bukzor - No recuerdo que me hayan preguntado si lo anterior es "razonable", por lo que no puedes decir que realmente "creo" que es - ¡FWIW, no lo es! :) por favor mira mi respuesta.
zzzeek
8

Me gustaría señalar que las soluciones dadas anteriormente no "simplemente funcionan" con consultas no triviales. Un problema que encontré fue tipos más complicados, como pgsql ARRAY que causan problemas. Encontré una solución que, para mí, funcionó incluso con las matrices pgsql:

prestado de: https://gist.github.com/gsakkis/4572159

El código vinculado parece estar basado en una versión anterior de SQLAlchemy. Recibirá un error que dice que el atributo _mapper_zero_or_none no existe. Aquí hay una versión actualizada que funcionará con una versión más nueva, simplemente reemplace _mapper_zero_or_none con bind. Además, esto tiene soporte para matrices pgsql:

# adapted from:
# https://gist.github.com/gsakkis/4572159
from datetime import date, timedelta
from datetime import datetime

from sqlalchemy.orm import Query


try:
    basestring
except NameError:
    basestring = str


def render_query(statement, dialect=None):
    """
    Generate an SQL expression string with bound parameters rendered inline
    for the given SQLAlchemy statement.
    WARNING: This method of escaping is insecure, incomplete, and for debugging
    purposes only. Executing SQL statements with inline-rendered user values is
    extremely insecure.
    Based on http://stackoverflow.com/questions/5631078/sqlalchemy-print-the-actual-query
    """
    if isinstance(statement, Query):
        if dialect is None:
            dialect = statement.session.bind.dialect
        statement = statement.statement
    elif dialect is None:
        dialect = statement.bind.dialect

    class LiteralCompiler(dialect.statement_compiler):

        def visit_bindparam(self, bindparam, within_columns_clause=False,
                            literal_binds=False, **kwargs):
            return self.render_literal_value(bindparam.value, bindparam.type)

        def render_array_value(self, val, item_type):
            if isinstance(val, list):
                return "{%s}" % ",".join([self.render_array_value(x, item_type) for x in val])
            return self.render_literal_value(val, item_type)

        def render_literal_value(self, value, type_):
            if isinstance(value, long):
                return str(value)
            elif isinstance(value, (basestring, date, datetime, timedelta)):
                return "'%s'" % str(value).replace("'", "''")
            elif isinstance(value, list):
                return "'{%s}'" % (",".join([self.render_array_value(x, type_.item_type) for x in value]))
            return super(LiteralCompiler, self).render_literal_value(value, type_)

    return LiteralCompiler(dialect, statement).process(statement)

Probado en dos niveles de matrices anidadas.

JamesHutchison
fuente
¿Muestra un ejemplo de cómo usarlo? Gracias
slashdottir
from file import render_query; print(render_query(query))
Alfonso Pérez
¡Ese es el único ejemplo de toda esta página que funcionó para mí! Gracias !
fougerejo