web-dev-qa-db-fra.com

SQLAlchemy: affiche la requête réelle

J'aimerais vraiment pouvoir imprimer du code SQL valide pour mon application, y compris des valeurs, plutôt que des paramètres de liaison, mais il n'est pas évident de savoir comment procéder dans SQLAlchemy (à dessein, j'en suis à peu près sûr).

Quelqu'un at-il résolu ce problème de manière générale?

134
bukzor

Cela fonctionne dans python 2 et 3 et est un peu plus propre qu'avant, mais requiert 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

Démo:

# 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 __== '__main__':
    test()

Donne cette sortie: (testé dans python 2.7 et 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
60
bukzor

Dans la grande majorité des cas, la "codification" d'une instruction ou d'une requête SQLAlchemy est aussi simple que:

print str(statement)

Ceci s’applique à la fois à un ORM Query ainsi qu’à toute instruction select() ou autre.

Remarque : la réponse détaillée suivante est conservée sur le documentation de sqlalchemy .

Pour obtenir l'instruction telle que compilée dans un dialecte ou un moteur spécifique, si l'instruction elle-même n'est pas déjà liée à un dialecte, vous pouvez la transmettre à compile () :

print statement.compile(someengine)

ou sans moteur:

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

Lorsqu'un objet ORM Query est attribué, pour accéder à la méthode compile(), nous avons uniquement besoin d'accéder à l'accesseur . Statement :

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

en ce qui concerne la stipulation initiale voulant que les paramètres liés soient "insérés" dans la chaîne finale, le problème est que SQLAlchemy n’est normalement pas chargé de cela, car cela est géré de manière appropriée par le Python DBAPI, sans parler des paramètres liés, est probablement la faille de sécurité la plus largement exploitée dans les applications Web modernes. SQLAlchemy a une capacité limitée à faire cette chaîne de caractères dans certaines circonstances telles que l’émission de DDL. Pour accéder à cette fonctionnalité, vous pouvez utiliser Le drapeau de literal_binds, passé à 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})

l'approche ci-dessus présente les mises en garde selon lesquelles il n'est pris en charge que pour les types de base, tels que ints et strings, et de plus, si un bindparam sans valeur prédéfinie est utilisé directement, il ne pourra pas en déduire que non plus.

Pour prendre en charge le rendu littéral en ligne pour les types non pris en charge, implémentez un TypeDecorator pour le type de cible, qui inclut une méthode TypeDecorator.process_literal_param:

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})
)

produire une sortie comme:

SELECT mytable.x
FROM mytable
WHERE mytable.x > my_fancy_formatting(5)
133
zzzeek

Étant donné que ce que vous voulez n'a de sens que lors du débogage, vous pouvez démarrer SQLAlchemy avec echo=True Pour consigner toutes les requêtes SQL. Par exemple:

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

Cela peut également être modifié pour une seule demande:

echo=False - si True, le moteur enregistre toutes les instructions ainsi qu'un repr() de leurs listes de paramètres dans le consignateur du moteur, dont la valeur par défaut est sys.stdout . L'attribut echo de Engine peut être modifié à tout moment pour activer et désactiver la journalisation. Si défini sur la chaîne "debug", Les lignes de résultat seront également imprimées sur la sortie standard. Cet indicateur contrôle en fin de compte un Python; voir Configuration de la journalisation ) pour obtenir des informations sur la configuration de la journalisation directement.

Source: configuration du moteur SQLAlchemy

Si utilisé avec Flask, vous pouvez simplement définir

app.config["SQLALCHEMY_ECHO"] = True

avoir le même comportement.

24
Vedran Šego

Nous pouvons utiliser la méthode compiler à cette fin. De la docs :

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}))

Résultat:

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

Donc, en me basant sur les commentaires de @ zzzeek sur le code de @ bukzor, je suis parvenu à ceci pour obtenir facilement une requête "jolie-imprimable":

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)

Personnellement, j'ai du mal à lire le code qui n'est pas mis en retrait et j'ai donc utilisé sqlparse pour réindentir le code SQL. Il peut être installé avec pip install sqlparse.

12
jmagnusson

Ce code est basé sur brillant réponse existante de @bukzor. Je viens d'ajouter un rendu personnalisé pour le type datetime.datetime Dans la TO_DATE() d'Oracle.

N'hésitez pas à mettre à jour le code correspondant à votre base de données:

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)
10
vvladymyrov

Je tiens à souligner que les solutions données ci-dessus ne "fonctionnent pas" avec des requêtes non triviales. Un problème que j'ai rencontré était des types plus compliqués, tels que pgsql ARRAYs provoquant des problèmes. J'ai trouvé une solution qui, pour moi, ne fonctionnait que même avec les tableaux de pgsql:

emprunté à: https://Gist.github.com/gsakkis/4572159

Le code lié semble être basé sur une version plus ancienne de SQLAlchemy. Vous obtiendrez une erreur indiquant que l'attribut _mapper_zero_or_none n'existe pas. Voici une version mise à jour qui fonctionnera avec une version plus récente, il vous suffit de remplacer _mapper_zero_or_none par bind. De plus, cela supporte les tableaux 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)

Testé sur deux niveaux de tableaux imbriqués.

6
JamesHutchison