SQLAlchemy: cetak kueri yang sebenarnya

165

Saya benar-benar ingin dapat mencetak SQL yang valid untuk aplikasi saya, termasuk nilai-nilai, daripada mengikat parameter, tetapi tidak jelas bagaimana melakukan ini dalam SQLAlchemy (secara desain, saya cukup yakin).

Adakah yang memecahkan masalah ini secara umum?

bukzor
sumber
1
Saya belum, tapi Anda mungkin bisa membangun solusi yang kurang rapuh dengan mengetuk sqlalchemy.enginelog SQLAlchemy . Ini mencatat kueri dan mengikat parameter, Anda hanya perlu mengganti placeholder mengikat dengan nilai-nilai pada string kueri SQL yang dibuat dengan mudah.
Simon
@Simon: ada dua masalah dengan menggunakan logger: 1) hanya mencetak ketika pernyataan dieksekusi 2) Saya masih harus melakukan penggantian string, kecuali dalam kasus itu, saya tidak akan tahu persis bind-template string , dan saya harus menguraikannya keluar dari teks kueri, membuat solusinya lebih rapuh.
bukzor
URL baru tampaknya docs.sqlalchemy.org/en/latest/faq/… untuk FAQ zzzeek.
Jim DeLaHunt

Jawaban:

167

Dalam sebagian besar kasus, "pengetatan" dari pernyataan atau permintaan SQLAlchemy semudah:

print str(statement)

Ini berlaku baik untuk ORM Querymaupun select()pernyataan apa pun atau lainnya.

Catatan : jawaban terinci berikut dijaga pada dokumentasi sqlalchemy .

Untuk mendapatkan pernyataan yang dikompilasi ke dialek atau mesin tertentu, jika pernyataan itu sendiri belum terikat dengan yang Anda bisa melewati ini untuk mengkompilasi () :

print statement.compile(someengine)

atau tanpa mesin:

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

Saat diberi Queryobjek ORM , untuk mendapatkan compile()metode ini, kita hanya perlu mengakses .statement accessor terlebih dahulu:

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

berkenaan dengan ketentuan asli bahwa parameter terikat harus "digariskan" ke dalam string terakhir, tantangan di sini adalah bahwa SQLAlchemy biasanya tidak ditugaskan dengan ini, karena ini ditangani dengan tepat oleh Python DBAPI, belum lagi melewati parameter terikat adalah mungkin lubang keamanan yang paling banyak dieksploitasi dalam aplikasi web modern. SQLAlchemy memiliki kemampuan terbatas untuk melakukan pengetatan ini dalam keadaan tertentu seperti memancarkan DDL. Untuk mengakses fungsi ini, seseorang dapat menggunakan bendera 'literal_binds', diteruskan ke 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})

pendekatan di atas memiliki peringatan bahwa itu hanya didukung untuk tipe dasar, seperti int dan string, dan lebih jauh lagi jika bindparam tanpa nilai yang telah ditetapkan sebelumnya digunakan secara langsung, itu tidak akan dapat untuk mengikat itu juga.

Untuk mendukung rendering literal sebaris untuk tipe yang tidak didukung, terapkan a TypeDecoratoruntuk tipe target yang mencakup TypeDecorator.process_literal_parammetode:

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

menghasilkan output seperti:

SELECT mytable.x
FROM mytable
WHERE mytable.x > my_fancy_formatting(5)
zzzeek
sumber
2
Ini tidak menempatkan tanda kutip di sekitar string, dan tidak menyelesaikan beberapa params terikat.
bukzor
1
bagian kedua dari jawabannya telah diperbarui dengan informasi terbaru.
zzzeek
2
@zzzeek Mengapa permintaan yang tidak tercetak cantik disertakan dalam sqlalchemy secara default? Seperti query.prettyprint(). Ini memudahkan rasa sakit debugging dengan pertanyaan besar.
jmagnusson
2
@jmagnusson karena kecantikan ada di mata yang melihatnya :) Ada banyak kait (mis. acara cursor_execute, filter pencatatan Python @compiles, dll.) untuk sejumlah paket pihak ketiga untuk menerapkan sistem pencetakan yang cantik.
zzzeek
1
@buzkor re: batas yang telah diperbaiki di 1.0 bitbucket.org/zzzeek/sqlalchemy/issue/3034/...
zzzeek
66

Ini berfungsi dalam python 2 dan 3 dan sedikit lebih bersih dari sebelumnya, tetapi membutuhkan 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()

Memberikan output ini: (diuji dengan python 2.7 dan 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
sumber
2
Ini luar biasa ... Harus menambahkan ini ke beberapa lib debug agar kami dapat dengan mudah mengaksesnya. Terima kasih telah melakukan gerakan kaki yang satu ini. Saya kagum bahwa itu harus sangat rumit.
Corey O.
5
Saya cukup yakin bahwa ini sengaja sulit, karena pemula tergoda untuk cursor.execute () string itu. Prinsip persetujuan orang dewasa umumnya digunakan dalam python.
bukzor
Sangat berguna. Terima kasih!
iklim
Memang sangat bagus. Saya mengambil kebebasan dan memasukkannya ke dalam stackoverflow.com/a/42066590/2127439 , yang mencakup SQLAlchemy v0.7.9 - v1.1.15, termasuk pernyataan INSERT dan UPDATE (PY2 / PY3).
wolfmanx
sangat bagus. tetapi apakah itu mengkonversi seperti di bawah ini. 1) query (Table) .filter (Table.Column1.is_ (False) ke WHERE Column1 IS 0. 2) query (Table) .filter (Table.Column1.is_ (Benar) ke WHERE Column1 IS 1. 3) query ( Tabel) .filter (Table.Column1 == func.any ([1,2,3])) ke WHERE Column1 = any ('[1,2,3]') konversi di atas tidak benar dalam sintaksis.
Sekhar C
51

Mengingat bahwa apa yang Anda inginkan hanya masuk akal ketika debugging, Anda dapat memulai SQLAlchemy dengan echo=True, untuk mencatat semua permintaan SQL. Sebagai contoh:

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

Ini juga dapat dimodifikasi hanya untuk satu permintaan:

echo=False- jika True, Engine akan mencatat semua pernyataan serta repr()daftar parameternya ke logger engine, yang defaultnya adalah sys.stdout. The echoatribut Enginedapat dimodifikasi setiap saat untuk menghidupkan logging dan mematikan. Jika diatur ke string "debug", baris hasil akan dicetak ke output standar juga. Bendera ini pada akhirnya mengontrol logger Python; lihat Mengkonfigurasi Logging untuk informasi tentang cara mengkonfigurasi logging secara langsung.

Sumber: Konfigurasi Mesin SQLAlchemy

Jika digunakan dengan Flask, Anda cukup mengatur

app.config["SQLALCHEMY_ECHO"] = True

untuk mendapatkan perilaku yang sama.

Vedran Šego
sumber
6
Jawaban ini layak menjadi jauh lebih tinggi .. dan untuk pengguna flask-sqlalchemyini harus jawaban yang diterima.
jso
25

Kita dapat menggunakan metode kompilasi untuk tujuan ini. Dari dokumen :

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

Hasil:

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

Peringatan dari dokumen:

Jangan pernah menggunakan teknik ini dengan konten string yang diterima dari input yang tidak dipercaya, seperti dari formulir web atau aplikasi input pengguna lainnya. Fasilitas SQLAlchemy untuk memaksa nilai-nilai Python menjadi nilai-nilai string SQL langsung tidak aman terhadap input yang tidak dipercaya dan tidak memvalidasi jenis data yang dilewatkan. Selalu gunakan parameter terikat ketika secara terprogram menerapkan pernyataan SQL non-DDL terhadap database relasional.

akshaynagpal
sumber
13

Jadi, membangun komentar @zzzeek pada kode @ bukzor, saya membuat ini untuk dengan mudah mendapatkan permintaan "cukup-cetak":

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)

Saya pribadi mengalami kesulitan membaca kode yang tidak berlekuk jadi saya sudah terbiasa sqlparsedengan reindent SQL. Dapat diinstal dengan pip install sqlparse.

jmagnusson
sumber
@bukzor Semua nilai berfungsi kecuali yang datatime.now()menggunakan python 3 + sqlalchemy 1.0. Anda harus mengikuti saran @zzzeek tentang cara membuat TypeDecorator khusus agar bisa berfungsi juga.
jmagnusson
Itu agak terlalu spesifik. Datetime tidak berfungsi dalam kombinasi python dan sqlalchemy. Juga, dalam py27, unicode non-ascii menyebabkan ledakan.
bukzor
Sejauh yang saya bisa lihat, rute TypeDecorator mengharuskan saya untuk mengubah definisi tabel saya, yang bukan persyaratan yang wajar untuk hanya melihat pertanyaan saya. Saya mengedit jawaban saya untuk sedikit lebih dekat dengan jawaban Anda dan zzzeek, ​​tetapi saya mengambil rute dialek khusus, yang benar-benar ortogonal ke definisi tabel.
bukzor
11

Kode ini didasarkan pada jawaban brilian yang ada dari @bukzor. Saya baru saja menambahkan render khusus untuk datetime.datetimetipe ke Oracle TO_DATE().

Jangan ragu untuk memperbarui kode yang sesuai dengan basis data Anda:

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
sumber
22
Saya tidak mengerti mengapa rakyat SA percaya bahwa operasi sederhana seperti itu sangat sulit .
bukzor
Terima kasih! render_literal_value bekerja dengan baik untuk saya. Satu-satunya perubahan saya adalah: return "%s" % valuealih-alih return repr(value)di bagian float, int, long karena Python menghasilkan long sebagai 22Lganti hanya22
OrganicPanda
Resep ini (dan juga yang asli) memunculkan UnicodeDecodeError jika nilai string bindparam tidak dapat diwakili dalam ascii. Saya memposting inti yang memperbaiki ini.
gsakkis
1
"STR_TO_DATE('%s','%%Y-%%m-%%d %%H:%%M:%%S')" % value.strftime("%Y-%m-%d %H:%M:%S")dalam mysql
Zitrax
1
@ Bukzor - Saya tidak ingat ditanya apakah hal di atas "masuk akal" sehingga Anda tidak bisa menyatakan bahwa saya "percaya" itu - FWIW, bukan! :) tolong lihat jawaban saya.
zzzeek
8

Saya ingin menunjukkan bahwa solusi yang diberikan di atas tidak "hanya bekerja" dengan pertanyaan non-sepele. Satu masalah yang saya temui adalah jenis yang lebih rumit, seperti pgsql ARRAY yang menyebabkan masalah. Saya menemukan solusi untuk saya, hanya bekerja dengan pgsql ARRAYs:

dipinjam dari: https://gist.github.com/gsakkis/4572159

Kode tertaut tampaknya didasarkan pada versi SQLAlchemy yang lebih lama. Anda akan mendapatkan pesan kesalahan yang mengatakan bahwa atribut _mapper_zero_or_none tidak ada. Berikut ini adalah versi yang diperbarui yang akan bekerja dengan versi yang lebih baru, Anda cukup mengganti _mapper_zero_or_none dengan bind. Selain itu, ini memiliki dukungan untuk array 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)

Diuji untuk dua level array bersarang.

JamesHutchison
sumber
Tolong tunjukkan contoh cara menggunakannya? Terima kasih
slashdottir
from file import render_query; print(render_query(query))
Alfonso Pérez
Itulah satu-satunya contoh dari seluruh halaman ini yang bekerja untuk saya! Terima kasih!
fougerejo