On Fri, Feb 8, 2019 at 10:05 AM Martin Stein <
mstei...@gmail.com> wrote:
>
> Am Donnerstag, 7. Februar 2019 23:45:09 UTC+1 schrieb Mike Bayer:
>>
>>
>>
>> note the mentioned issue is closed. IN supports an inline parameter now:
>>
>>
https://docs.sqlalchemy.org/en/rel_1_2/orm/extensions/baked.html#baked-in
>>
>
> We had started using the .op('IN') approach around v1.1 and I hadn't realized that SQLAlchemy supports this now. Very good to know, thanks.
>
> I have integrated your suggested approach with the 'before_cursor_execute' rewrite of the statement, and it works nicely, thanks again!
>
> Simply out of curiosity and because I'm trying to understand the internals a bit better:
>
> 1) Theoretically, would another possible approach be to implement a sub-class of BindParameter (let's say PGValuesBindParameter) and - in combination with the .op('IN') approach - overwrite the bind-parameter behavior so it passes the values as one long string like this:
> .filter(Item.parent_id.op('IN')(PGValuesBindParameter('parent_ids')))
>
> # Parameters that are passed into the statement:
> {
> 'parent_ids': "(VALUES ('abc'), ('def'), ('gjh'))"
> }
so the event recipe I gave you , you probably *want* to do this, so
that in the event, you not only look at bindparam.expanding, you also
do isinstance(param, PGValuesBindParameter).
>
> 2) I've also considered using the following construct with the array-function from the postgres-dialect. It achieves the same performance improvement as the VALUES approach and doesn't require the tricky rewriting of the VALUES-part with the inner parentheses:
>
> func.unnest(array(parent_ids))
>
> but I couldn't get it to work with bindparams (tried also with expanding=True).
>
> Which one would you suggest/do you think is the cleanest... the VALUES statement-rewrite, or one of those other approaches?
ideally the expanding IN system in SQLAlchemy would have a little bit
of expansion built into it so you could just get in there to add this
keyword.
the big issue here is that you need to use the baked system so any
writing of these parameters / arrays / whatever needs to happen
*after* SQLAlchemy creates the string. the current IN trick you are
doing is taking advantage of psycopg2's ability to pass a tuple as a
parameter to libpq. psycopg2 probably can't achieve this with
"UNNEST" because that's a SQL function, not something that libpq can
accept as a parameter.
So rewriting the statement after the fact is the best way to go.
the workings of it are complicated because we have to also take your
single parameter "foo" and add "foo_1", "foo_2", "foo_3", etc. to the
parameter dictionary. If you are then passing in a list of tuples,
then it has to break into a list of tuples. The logic is at
https://github.com/sqlalchemy/sqlalchemy/blob/master/lib/sqlalchemy/engine/default.py#L748,
you can see it's pretty tedious, so it's probably best to keep
leveraging that logic.
Looking at the logic there, I can see we can leverage the existing
"render a tuple" logic so that we can remove the regexp thing if you
just pass the params as tuples. This can also be automated if you
rewrite the parameter dictionary using the before_execute() event, but
here's the more simple idea:
from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy.ext import baked
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import BindParameter
class PGValuesParam(BindParameter):
pass
@compiles(PGValuesParam)
def _compile_pgvalues(element, compiler, **kw):
string = compiler.visit_bindparam(element, **kw)
return "(VALUES %s" % string[1:]
Base = declarative_base()
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
data = Column(String)
e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
Base.metadata.create_all(e)
s = Session(e)
bakery = baked.bakery()
query = bakery(
lambda s: s.query(A).filter(A.id.in_(PGValuesParam("ids", expanding=True)))
)
q = query(s).params(ids=[(1,), (2,), (3,)])
print(q)
q.all()