SQLAlchemy: Druckt die eigentliche Abfrage

164

Ich möchte wirklich in der Lage sein, gültiges SQL für meine Anwendung, einschließlich Werte, auszudrucken, anstatt Parameter zu binden, aber es ist nicht offensichtlich, wie dies in SQLAlchemy gemacht wird (ich bin mir ziemlich sicher, dass dies beabsichtigt ist).

Hat jemand dieses Problem allgemein gelöst?

Bukzor
quelle
1
Ich habe es nicht getan, aber Sie könnten wahrscheinlich eine weniger fragile Lösung erstellen, indem Sie auf das sqlalchemy.engineProtokoll von SQLAlchemy tippen . Es protokolliert Abfragen und Bindungsparameter. Sie müssen lediglich die Bindungsplatzhalter durch die Werte in einer leicht zu erstellenden SQL-Abfragezeichenfolge ersetzen.
Simon
@Simon: Es gibt zwei Probleme bei der Verwendung des Loggers: 1) Er wird nur gedruckt, wenn eine Anweisung ausgeführt wird. 2) Ich müsste immer noch einen String ersetzen, außer in diesem Fall würde ich den Bind-Template-String nicht genau kennen und ich muss irgendwie der Abfrage Text parsen, die Lösung macht mehr zerbrechlich.
Bukzor
Die neue URL scheint docs.sqlalchemy.org/en/latest/faq/… für die FAQ von @ zzzeek zu sein.
Jim DeLaHunt

Antworten:

166

In den allermeisten Fällen ist die "Stringifizierung" einer SQLAlchemy-Anweisung oder -Abfrage so einfach wie:

print str(statement)

Dies gilt sowohl für ein ORM Queryals auch für eine select()oder andere Aussage.

Hinweis : Die folgende detaillierte Antwort wird in der sqlalchemy-Dokumentation beibehalten .

Um die Anweisung so zu kompilieren, dass sie für einen bestimmten Dialekt oder eine bestimmte Engine kompiliert wurde, können Sie diese an compile () übergeben , wenn die Anweisung selbst noch nicht an eine gebunden ist :

print statement.compile(someengine)

oder ohne Motor:

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

Wenn wir ein ORM- QueryObjekt erhalten, müssen compile()wir zuerst nur auf den .statement- Accessor zugreifen , um an die Methode zu gelangen :

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

In Bezug auf die ursprüngliche Bestimmung, dass gebundene Parameter in die endgültige Zeichenfolge "eingebunden" werden sollen, besteht die Herausforderung darin, dass SQLAlchemy normalerweise nicht damit beauftragt ist, da dies vom Python-DBAPI angemessen behandelt wird, ganz zu schweigen von der Umgehung gebundener Parameter wahrscheinlich die am häufigsten genutzten Sicherheitslücken in modernen Webanwendungen. SQLAlchemy ist unter bestimmten Umständen, z. B. bei der Emission von DDL, nur eingeschränkt in der Lage, diese Stringifizierung durchzuführen. Um auf diese Funktionalität zuzugreifen, kann das Flag 'literal_binds' verwendet werden, das an Folgendes übergeben wird 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})

Der obige Ansatz hat die Einschränkungen, dass er nur für Basistypen wie Ints und Strings unterstützt wird. Wenn ein bindparam Wert ohne voreingestellten Wert direkt verwendet wird, kann er dies auch nicht stringifizieren.

Um das Inline-Literal-Rendering für nicht unterstützte Typen zu unterstützen, implementieren Sie ein TypeDecoratorfür den Zieltyp, das eine TypeDecorator.process_literal_paramMethode enthält:

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

Produktion produzieren wie:

SELECT mytable.x
FROM mytable
WHERE mytable.x > my_fancy_formatting(5)
zzzeek
quelle
2
Dies setzt keine Anführungszeichen um Zeichenfolgen und löst einige gebundene Parameter nicht auf.
Bukzor
1
Die zweite Hälfte der Antwort wurde mit den neuesten Informationen aktualisiert.
Zzzeek
2
@zzzeek Warum sind in sqlalchemy standardmäßig keine hübschen Druckabfragen enthalten? Wie query.prettyprint(). Es lindert das Debuggen bei großen Abfragen immens.
jmagnusson
2
@jmagnusson, weil Schönheit im Auge des Betrachters liegt :) Es gibt zahlreiche Hooks (z. B. cursor_execute-Ereignis, Python-Protokollierungsfilter @compilesusw.) für eine beliebige Anzahl von Paketen von Drittanbietern, um hübsche Drucksysteme zu implementieren.
Zzzeek
1
@buzkor re: Limit, das in 1.0 bitbucket.org/zzzeek/sqlalchemy/issue/3034/…
zzzeek
66

Dies funktioniert in Python 2 und 3 und ist etwas sauberer als zuvor, erfordert jedoch 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

Demo:

# 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()

Gibt diese Ausgabe: (getestet in Python 2.7 und 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
quelle
2
Das ist großartig ... Muss dies zu einigen Debug-Bibliotheken hinzufügen, damit wir leicht darauf zugreifen können. Vielen Dank für die Beinarbeit. Ich bin erstaunt, dass es so kompliziert sein musste.
Corey O.
5
Ich bin mir ziemlich sicher, dass dies absichtlich schwierig ist, da Neulinge versucht sind, diesen String mit cursor.execute () zu versehen. Das Prinzip der Zustimmung von Erwachsenen wird jedoch häufig in Python verwendet.
Bukzor
Sehr hilfreich. Vielen Dank!
Klima
Wirklich sehr nett. Ich habe mir die Freiheit genommen und dies in stackoverflow.com/a/42066590/2127439 integriert , das SQLAlchemy v0.7.9 - v1.1.15 einschließlich INSERT- und UPDATE-Anweisungen (PY2 / PY3) behandelt.
Wolfmanx
Sehr schön. aber konvertiert es wie unten. 1) Abfrage (Tabelle) .filter (Table.Column1.is_ (False) nach WHERE Column1 IS 0. 2) Abfrage (Table) .filter (Table.Column1.is_ (True) nach WHERE Column1 IS 1. 3) Abfrage ( Tabelle) .filter (Table.Column1 == func.any ([1,2,3])) to WHERE Column1 = any ('[1,2,3]') über Konvertierungen sind in der Syntax falsch.
Sekhar C
50

Da das, was Sie möchten, nur beim Debuggen sinnvoll ist, können Sie SQLAlchemy mit starten echo=True, um alle SQL-Abfragen zu protokollieren. Beispielsweise:

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

Dies kann auch nur für eine einzelne Anfrage geändert werden:

echo=False- Wenn Trueja, protokolliert die Engine alle Anweisungen sowie eine repr()ihrer Parameterlisten im Engineers-Logger, der standardmäßig verwendet wird sys.stdout. Das echoAttribut von Enginekann jederzeit geändert werden, um die Protokollierung ein- und auszuschalten. Wenn die Zeichenfolge festgelegt ist "debug", werden die Ergebniszeilen auch in der Standardausgabe gedruckt. Dieses Flag steuert letztendlich einen Python-Logger. siehe Konfigurieren der Protokollierung für Informationen zum Konfigurieren Protokollierung direkt.

Quelle: SQLAlchemy Engine-Konfiguration

Bei Verwendung mit Flask können Sie einfach einstellen

app.config["SQLALCHEMY_ECHO"] = True

das gleiche Verhalten zu bekommen.

Vedran Šego
quelle
6
Diese Antwort verdient es, viel höher zu sein. Und für Benutzer flask-sqlalchemysollte dies die akzeptierte Antwort sein.
Also
24

Zu diesem Zweck können wir die Kompilierungsmethode verwenden. Aus den Dokumenten :

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

Ergebnis:

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

Warnung aus Dokumenten:

Verwenden Sie diese Technik niemals mit Zeichenfolgeninhalten, die von nicht vertrauenswürdigen Eingaben empfangen werden, z. B. von Webformularen oder anderen Benutzereingabeanwendungen. Die Funktionen von SQLAlchemy zum Erzwingen von Python-Werten in direkte SQL-Zeichenfolgenwerte sind nicht gegen nicht vertrauenswürdige Eingaben geschützt und validieren den übergebenen Datentyp nicht. Verwenden Sie immer gebundene Parameter, wenn Sie programmgesteuert Nicht-DDL-SQL-Anweisungen für eine relationale Datenbank aufrufen.

akshaynagpal
quelle
13

Aufbauend auf den Kommentaren von @ zzzeek zum Code von @ bukzor habe ich mir diese ausgedacht, um leicht eine "ziemlich druckbare" Abfrage zu erhalten:

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)

Ich persönlich habe sqlparseSchwierigkeiten, Code zu lesen, der nicht eingerückt ist, daher habe ich SQL neu eingerückt. Es kann mit installiert werden pip install sqlparse.

jmagnusson
quelle
@bukzor Alle Werte außer dem datatime.now()bei Verwendung von Python 3 + sqlalchemy 1.0 funktionieren. Sie müssten den Ratschlägen von @ zzzeek zum Erstellen eines benutzerdefinierten TypeDecorators folgen, damit dieser auch funktioniert.
jmagnusson
Das ist etwas zu spezifisch. Die datetime funktioniert in keiner Kombination aus Python und SQLalchemie. In py27 verursacht der Nicht-ASCII-Unicode eine Explosion.
Bukzor
Soweit ich sehen konnte, muss ich für die TypeDecorator-Route meine Tabellendefinitionen ändern. Dies ist keine vernünftige Voraussetzung, um nur meine Abfragen anzuzeigen. Ich habe meine Antwort so bearbeitet, dass sie Ihrer und der von zzzeek etwas näher kommt, aber ich habe den Weg eines benutzerdefinierten Dialekts eingeschlagen, der richtig orthogonal zu den Tabellendefinitionen ist.
Bukzor
11

Dieser Code basiert auf einer brillanten Antwort von @bukzor. Ich habe gerade ein benutzerdefiniertes Rendering für den datetime.datetimeTyp in Oracle hinzugefügt TO_DATE().

Fühlen Sie sich frei, den Code entsprechend Ihrer Datenbank zu aktualisieren:

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
quelle
22
Ich verstehe nicht, warum die SA-Leute glauben, dass es vernünftig ist, dass eine so einfache Operation so schwierig ist .
Bukzor
Danke dir! render_literal_value hat bei mir gut funktioniert. Meine einzige Änderung war: return "%s" % valuestatt return repr(value)im Abschnitt float, int, long, weil Python Longs 22L22
ausgab
Dieses Rezept (wie auch das Original) löst UnicodeDecodeError aus, wenn ein Bindparam-String-Wert in ASCII nicht darstellbar ist. Ich habe einen Kern gepostet , der dies behebt.
Gsakkis
1
"STR_TO_DATE('%s','%%Y-%%m-%%d %%H:%%M:%%S')" % value.strftime("%Y-%m-%d %H:%M:%S")in MySQL
Zitrax
1
@bukzor - Ich kann mich nicht erinnern, gefragt worden zu sein, ob das oben Genannte "vernünftig" ist. Sie können also nicht wirklich sagen, dass ich es "glaube" - FWIW, das ist es nicht! :) Bitte siehe meine Antwort.
Zzzeek
8

Ich möchte darauf hinweisen, dass die oben angegebenen Lösungen nicht "nur" mit nicht trivialen Abfragen funktionieren. Ein Problem, auf das ich gestoßen bin, waren kompliziertere Typen, wie z. B. pgsql ARRAYs, die Probleme verursachen. Ich habe eine Lösung gefunden, die für mich sogar mit pgsql ARRAYs funktioniert hat:

ausgeliehen von: https://gist.github.com/gsakkis/4572159

Der verknüpfte Code scheint auf einer älteren Version von SQLAlchemy zu basieren. Sie erhalten eine Fehlermeldung, dass das Attribut _mapper_zero_or_none nicht vorhanden ist. Hier ist eine aktualisierte Version, die mit einer neueren Version funktioniert. Sie ersetzen einfach _mapper_zero_or_none durch bind. Darüber hinaus werden pgsql-Arrays unterstützt:

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

Getestet auf zwei Ebenen verschachtelter Arrays.

James Hutchison
quelle
Bitte zeigen Sie ein Beispiel für die Verwendung. Vielen Dank
Slashdottir
from file import render_query; print(render_query(query))
Alfonso Pérez
Das ist das einzige Beispiel für diese ganze Seite, das für mich funktioniert hat! Vielen Dank !
Fougerejo