287

I'd really like to be able to print out valid SQL for my application, including values, rather than bind parameters, but it's not obvious how to do this in SQLAlchemy (by design, I'm fairly sure).

Has anyone solved this problem in a general way?

bukzor
  • 37,539
  • 11
  • 77
  • 111
  • 1
    I haven't, but you could probably build a less fragile solution by tapping into SQLAlchemy's `sqlalchemy.engine` log. It logs queries and bind parameters, you'd only have to replace the bind placeholders with the values on a readily constructed SQL query string. – Simon Apr 12 '11 at 08:22
  • @Simon: there's two problems with using the logger: 1) it only prints when a statement is *executing* 2) I'd still have to do a string replace, except in that case, I wouldn't know the bind-template string exactly, and I'd have to somehow parse it out of the query text, making the solution *more* fragile. – bukzor Apr 12 '11 at 16:14
  • The new URL appears to be http://docs.sqlalchemy.org/en/latest/faq/sqlexpressions.html#how-do-i-render-sql-expressions-as-strings-possibly-with-bound-parameters-inlined for @zzzeek's FAQ. – Jim DeLaHunt Jun 24 '15 at 18:53

11 Answers11

304

In the vast majority of cases, the "stringification" of a SQLAlchemy statement or query is as simple as:

print(str(statement))

This applies both to an ORM Query as well as any select() or other statement.

Note: the following detailed answer is being maintained on the sqlalchemy documentation.

To get the statement as compiled to a specific dialect or engine, if the statement itself is not already bound to one you can pass this in to compile():

print(statement.compile(someengine))

or without an engine:

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

When given an ORM Query object, in order to get at the compile() method we only need access the .statement accessor first:

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

with regards to the original stipulation that bound parameters are to be "inlined" into the final string, the challenge here is that SQLAlchemy normally is not tasked with this, as this is handled appropriately by the Python DBAPI, not to mention bypassing bound parameters is probably the most widely exploited security holes in modern web applications. SQLAlchemy has limited ability to do this stringification in certain circumstances such as that of emitting DDL. In order to access this functionality one can use the 'literal_binds' flag, passed to 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}))

the above approach has the caveats that it is only supported for basic types, such as ints and strings, and furthermore if a bindparam without a pre-set value is used directly, it won't be able to stringify that either.

To support inline literal rendering for types not supported, implement a TypeDecorator for the target type which includes a TypeDecorator.process_literal_param method:

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

producing output like:

SELECT mytable.x
FROM mytable
WHERE mytable.x > my_fancy_formatting(5)
Ilja Everilä
  • 50,538
  • 7
  • 126
  • 127
zzzeek
  • 72,307
  • 23
  • 193
  • 185
  • We're stuck on 0.7 for now, so I still need to use my own answer. – bukzor Aug 01 '14 at 16:30
  • Even in 0.9, this answer leaves any limit clause with a placeholder. – bukzor Aug 01 '14 at 16:48
  • 10
    @zzzeek Why isn't pretty-printing queries included in sqlalchemy by default? Like `query.prettyprint()`. It eases the debugging pain with big queries immensely. – jmagnusson Aug 08 '14 at 10:39
  • 4
    @jmagnusson because beauty is in the eye of the beholder :) There are ample hooks (e.g. cursor_execute event, Python logging filters, `@compiles`, etc.) for any number of third party packages to implement pretty-printing systems. – zzzeek Aug 08 '14 at 14:33
  • 1
    @buzkor re: limit that's been fixed in 1.0 https://bitbucket.org/zzzeek/sqlalchemy/issue/3034/use-my-own-bindparam-for-querylimit – zzzeek Aug 08 '14 at 14:36
  • 9
    For me it was looking for `from sqlalchemy.dialects import postgresql; print(query.statement.compile(dialect=postgresql.dialect(), compile_kwargs={"literal_binds": True})))` – Martin Thoma Aug 22 '19 at 13:13
  • print(str(query.statement)) worked for me, thanks. – fudu Jun 07 '23 at 08:05
141

Given that what you want makes sense only when debugging, you could start SQLAlchemy with echo=True, to log all SQL queries. For example:

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

This can also be modified for just a single request:

echo=False – if True, the Engine will log all statements as well as a repr() of their parameter lists to the engines logger, which defaults to sys.stdout. The echo attribute of Engine can be modified at any time to turn logging on and off. If set to the string "debug", result rows will be printed to the standard output as well. This flag ultimately controls a Python logger; see Configuring Logging for information on how to configure logging directly.

Source: SQLAlchemy Engine Configuration

If used with Flask, you can simply set

app.config["SQLALCHEMY_ECHO"] = True

to get the same behaviour.

Community
  • 1
  • 1
Vedran Šego
  • 3,553
  • 3
  • 27
  • 40
  • 18
    This answer deserves to be way higher.. and for users of `flask-sqlalchemy` this should be the accepted answer. – jso Oct 29 '18 at 16:22
  • Yes, this is the "correct" answer. Maybe it did not exist back in 2014?? – Mike Williamson Sep 30 '20 at 14:51
  • @MikeWilliamson, you can find it used [here](https://docs.sqlalchemy.org/en/13/changelog/migration_08.html), which is a document from Oct 2012, updated in Mar 2013. Admittedly, that is newer than the question (which is from Apr 2011). – Vedran Šego Oct 01 '20 at 15:09
78

This works in python 2 and 3 and is a bit cleaner than before, but requires 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()

Gives this output: (tested in python 2.7 and 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
  • 37,539
  • 11
  • 77
  • 111
  • 7
    This is awesome ... Will have to add this to some debug libs so that we can easily access it. Thanks for doing the footwork on this one. I'm amazed that it had to be so complicated. – Corey O. May 24 '12 at 18:26
  • 8
    I'm pretty sure that this is intentionally hard, because newbies are tempted to cursor.execute() that string. The principle of consenting adults is commonly used in python though. – bukzor May 25 '12 at 20:44
  • Very nice indeed. I took the liberty and incorporated this into http://stackoverflow.com/a/42066590/2127439, which covers SQLAlchemy v0.7.9 - v1.1.15, including INSERT and UPDATE statements (PY2/PY3). – wolfmanx Mar 07 '17 at 04:19
  • very nice. but is it converting as below. 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])) to WHERE Column1 = any('[1,2,3]') above conversions are incorrect in syntax. – Sekhar C May 30 '20 at 16:30
  • How to solve default value in insert statement? ``` python from sqlalchemy import MetaData, insert from sqlalchemy.schema import Column, Table meta = MetaData() TGR = Table('tbl', meta, Column('a'), Column('b'), Column('c', default='xxx') ) s = TGR.insert().values(a=1, b=2) print(literalquery(s)) ``` INSERT INTO tbl (a, b, c) VALUES (1, 2, :c) – janezj Jan 08 '21 at 11:50
  • This is by far my favourite solution, but recently I've been getting the following error: __init__() got an unexpected keyword argument '_enums' Any idea why it can't compile the query? – c8999c 3f964f64 Jan 29 '21 at 16:15
56

We can use compile method for this purpose. From the 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}))

Result:

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

Warning from docs:

Never use this technique with string content received from untrusted input, such as from web forms or other user-input applications. SQLAlchemy’s facilities to coerce Python values into direct SQL string values are not secure against untrusted input and do not validate the type of data being passed. Always use bound parameters when programmatically invoking non-DDL SQL statements against a relational database.

SuperShoot
  • 9,880
  • 2
  • 38
  • 55
akshaynagpal
  • 2,965
  • 30
  • 32
  • For dialect please refer to this page: https://docs.sqlalchemy.org/en/13/dialects/ – Taras Jun 02 '21 at 06:14
  • 1
    this is the best answer. – Ping.Goblue Jan 26 '22 at 07:00
  • 1
    You may run into `*** NotImplementedError: Don't know how to literal-quote value '2022-09-29 18:50:00 +0200'` – tread Oct 27 '22 at 07:24
  • 1
    @tread , I am using a combination of this answer and bukzor's answer below for this reason. Both solutions produce errors in some cases, so I just do a try - except with both dialects in succession, the mysql / postgres dialect first, followed by the LiteralDialect (see below) – c8999c 3f964f64 Nov 04 '22 at 13:36
18

So building on @zzzeek's comments on @bukzor's code I came up with this to easily get a "pretty-printable" query:

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)

I personally have a hard time reading code which is not indented so I've used sqlparse to reindent the SQL. It can be installed with pip install sqlparse.

jmagnusson
  • 5,799
  • 4
  • 43
  • 38
  • @bukzor All values work except the `datatime.now()` one when using python 3 + sqlalchemy 1.0. You would have to follow @zzzeek's advice on creating a custom TypeDecorator for that one to work as well. – jmagnusson May 13 '15 at 12:08
  • That's a little too specific. The datetime doesn't work in any combination of python and sqlalchemy. Also, in py27, the non-ascii unicode causes an explosion. – bukzor Jun 24 '15 at 21:05
  • As far as I could see, the TypeDecorator route requires me to alter my table definitions, which isn't a reasonable requirement to simply see my queries. I edited my answer to be a bit closer to yours and zzzeek's, but I took the route of a custom dialect, which is properly orthogonal to the table definitions. – bukzor Jun 26 '15 at 16:54
14

This code is based on brilliant existing answer from @bukzor. I just added custom render for datetime.datetime type into Oracle's TO_DATE().

Feel free to update code to suit your database:

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)
Community
  • 1
  • 1
vvladymyrov
  • 5,715
  • 2
  • 32
  • 50
  • 45
    I don't see why the SA folk believe it's reasonable for such a simple operation to be *so hard*. – bukzor Mar 28 '12 at 23:31
  • Thank you! render_literal_value worked well for me. My only change was: `return "%s" % value` instead of `return repr(value)` in the float, int, long section because Python was outputting longs as `22L` instead of just `22` – OrganicPanda Jun 20 '12 at 09:55
  • This recipe (as well as the original) raises UnicodeDecodeError if any bindparam string value is not representable in ascii. I posted a [gist](https://gist.github.com/4572159) that fixes this. – gsakkis Jan 19 '13 at 11:34
  • 1
    `"STR_TO_DATE('%s','%%Y-%%m-%%d %%H:%%M:%%S')" % value.strftime("%Y-%m-%d %H:%M:%S")` in mysql – Zitrax Nov 08 '13 at 10:20
  • 1
    @bukzor - I don't recall being asked if the above is "reasonable" so you can't really state that I "believe" it is - FWIW, it's not! :) please see my answer. – zzzeek May 23 '14 at 18:54
  • @gsakkis: As far as I can see, your fix uses str(), which uses the ascii codec. How does your change fix the issue? – bukzor Aug 01 '14 at 16:34
  • How do you know what the Python DBAPI driver for Oracle uses for bind params of `datetime.datetime` type? Why do you think it's `TO_DATE()`? – Piotr Dobrogost Oct 18 '16 at 15:56
11

I would like to point out that the solutions given above do not "just work" with non-trivial queries. One issue I came across were more complicated types, such as pgsql ARRAYs causing issues. I did find a solution that for me, did just work even with pgsql ARRAYs:

borrowed from: https://gist.github.com/gsakkis/4572159

The linked code seems to be based on an older version of SQLAlchemy. You'll get an error saying that the attribute _mapper_zero_or_none doesn't exist. Here's an updated version that will work with a newer version, you simply replace _mapper_zero_or_none with bind. Additionally, this has support for pgsql arrays:

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

Tested to two levels of nested arrays.

Thomas Grainger
  • 2,271
  • 27
  • 34
JamesHutchison
  • 885
  • 2
  • 8
  • 17
6

To log SQL queries using Python logging instead of the echo=True flag:

import logging

logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)

per the documentation.

EliadL
  • 6,230
  • 2
  • 26
  • 43
3

Just a simple colored example with ORM's Query and pygments.

import sqlparse
from pygments import highlight
from pygments.formatters.terminal import TerminalFormatter
from pygments.lexers import SqlLexer
from sqlalchemy import create_engine
from sqlalchemy.orm import Query

engine = create_engine("sqlite+pysqlite:///db.sqlite", echo=True, future=True)

def format_sql(query: Query):
    compiled = query.statement.compile(
         engine, compile_kwargs={"literal_binds": True})
    parsed = sqlparse.format(str(compiled), reindent=True, keyword_case='upper')
    print(highlight(parsed, SqlLexer(), TerminalFormatter()))

Or version without sqlparse (without sqlparse there are less new lines in output)

def format_sql(query: Query):
    compiled = query.statement.compile(
        engine, compile_kwargs={"literal_binds": True})
    print(highlight(str(compiled), SqlLexer(), TerminalFormatter()))
Mark Mishyn
  • 3,921
  • 2
  • 28
  • 30
0

For some reason, print(str(stmt)) did not work for me.

(Perhaps because I'm trying to print while running tests? I have found debugging print capabilities to be slightly affected.)

Interestingly however, this worked:

stmt.__str__()
half of a glazier
  • 1,864
  • 2
  • 15
  • 45
-1

This is my approach

# query is instance of: from sqlalchemy import select
def raw_query(query):
    q = str(query.compile())
    p = query.compile().params
    for k in p.keys():
        v = p.get(k)
        if isinstance(v, (int, float, complex)):
            q = q.replace(f":{k}", f"{v}")
        else:
            q = q.replace(f":{k}", f"'{v}'")
    print(q)

How to use it:

from sqlalchemy import select

select_query = select([
    any_model_table.c["id_account"],
    any_model_table.c["id_provider"],
    any_model_table.c["id_service"],
    func.sum(any_model_table.c["items"]).label("items"),
    # @eaf
    func.date_format(func.now(), "%Y-%m-%d").label("some_date"),
    func.date_format(func.now(), "%Y").label("as_year"),
    func.date_format(func.now(), "%m").label("as_month"),
    func.date_format(func.now(), "%d").label("as_day"),
]).group_by(
    any_model_table.c.id_account,
    any_model_table.c.id_provider,
    any_model_table.c.id_service
).where(
    any_model_table.c.id == 5

).where(
    func.date_format(any_model_table.c.dt, "%Y-%m-%d") == datetime.utcnow().strftime('%Y-%m-%d')
)

raw_query(select_query)
ioedu
  • 27
  • 8