Hi all,

I'd like to learn a trick that would allow me to don't-repeat-myself: in my
current project, I have the noble goal of replacing MySQL with PostgreSQL. As
this is obviously an on-going effort, I need to keep existing code in working
order.

I have several queries built dynamically, using a "global" variable that
specifies the target engine, so my code is something like

  PG_ENGINE = 'PostgreSQL'
  MS_ENGINE = 'MySQL'

  DB_ENGINE = PG_ENGINE

  somequery = sa.select([foo.c.id, foo.c.description],
                        from_obj=foo.join(bar)...).where(...)
  
  if DB_ENGINE is PG_ENGINE:
    somequery.append_column(foo.c.pg_specific_field.label('field'))
  elif DB_ENGINE is MS_ENGINE:
    somequery.append_column(foo.c.ms_specific_field.label('field'))

In a few cases, those specific fields come from a PostgreSQL function that
returns multiple values, so I'd need to add a "LEFT JOIN function(xx,yy)" but
so far I wasn't able to find a way to do that. To keep going, I used
"append_from()" to add the function, but it's not ideal (nor right, probably).

Here is an actual example:

  import sqlalchemy as sa

  e = sa.create_engine('postgresql://localhost/tests')
  md = sa.MetaData(bind=e)

  kinds = sa.Table('kinds', md,
                   sa.Column('id', sa.Integer, primary_key=True),
                   sa.Column('description', sa.String(64)))

  products = sa.Table('products', md,
                      sa.Column('id', sa.Integer, primary_key=True),
                      sa.Column('description', sa.String(64)),
                      sa.Column('idkind', sa.Integer, 
sa.ForeignKey('kinds.id')))


  md.create_all()

  c = e.connect()
  with c.begin():
      c.execute("""
      CREATE OR REPLACE FUNCTION price_and_discount(
          in p_idproduct integer, p_idcustomer integer,
          out price numeric(15,4), out discount numeric(7,4)
      ) AS $$
      BEGIN
        -- determine price and discount for the given customer
        price := 123.456 * p_idproduct;
        discount := 12.34;
        RETURN;
      END;
      $$ LANGUAGE plpgsql
      """)

      c.execute('truncate kinds cascade')
      c.execute('truncate products cascade')

  insert_kind = kinds.insert()
  insert_prod = products.insert()

  with c.begin():
      c.execute(insert_kind, id=1, description='Kind of foos')
      c.execute(insert_kind, id=2, description='Kind of bars')
      c.execute(insert_prod, id=1, description='Foo', idkind=1)
      c.execute(insert_prod, id=2, description='Bar', idkind=2)

  # This is what I'd like to obtain

  raw_query = """\
  SELECT p.id, p.description, k.description AS kind, pad.price, pad.discount
  FROM products AS p
    JOIN kinds k ON k.id = p.idkind
    LEFT JOIN price_and_discount(p.id, 100) AS pad ON true"""

  print("\n\nRaw query:\n%s\n" % raw_query)
  res = c.execute(raw_query)
  print(res.fetchall())

  # This produces the same result, *in this particular case*

  p = products.alias('p')
  k = kinds.alias('k')
  pad = sa.func.price_and_discount(
          p.c.id,
          sa.bindparam('idcustomer')).alias('pad')

  query = sa.select([p.c.id, p.c.description, k.c.description.label('kind')],
                    from_obj=p.join(k))

  query.append_from(pad)
  query.append_column(sa.sql.column('pad.price', 
is_literal=True).label('price'))
  query.append_column(sa.sql.column('pad.discount', 
is_literal=True).label('discount'))
  print("\n\nCross-table query:\n%s\n" % query)
  res = c.execute(query, idcustomer=100)
  print(res.fetchall())

  # This is wrong, it outputs a cross-table result

  query = sa.select([p.c.id, p.c.description, k.c.description.label('kind')],
                    from_obj=p.join(k))

  query.append_from(p.outerjoin(pad, sa.text('true')))
  query.append_column(sa.sql.column('pad.price', 
is_literal=True).label('price'))
  query.append_column(sa.sql.column('pad.discount', 
is_literal=True).label('discount'))
  print("\n\nJoined query:\n%s\n" % query)
  res = c.execute(query, idcustomer=100)
  print(res.fetchall())

The last case in particular creates the following query (slightly edited for 
readability):

  SELECT p.id, p.description, k.description AS kind, pad.price AS price,
         pad.discount AS discount  
  FROM products AS p JOIN kinds AS k ON k.id = p.idkind,
       products AS p
         LEFT OUTER JOIN price_and_discount(p.id, %(idcustomer)s) AS pad ON true

that clearly raises an 'ambiguous table "p"' psycopg2.ProgrammingError. I
tried playing with .correlate(), but without luck, so I'm assuming this is
plain wrong.

Curiously, with a simpler base query such as

  query = sa.select([p], from_obj=p)
  query.append_from(p.outerjoin(pad, sa.text('true')))

it instead emits the right SQL, that is:

  SELECT p.id, p.description, p.idkind, pad.price AS price, pad.discount AS 
discount 
  FROM products AS p LEFT OUTER JOIN price_and_discount(p.id, %(idcustomer)s) 
AS pad ON true

So, is there a "correct way" to dynamically inject an outer join into an
existing base query?

Thanks in advance for any hint,
ciao, lele.
-- 
nickname: Lele Gaifax | Quando vivrò di quello che ho pensato ieri
real: Emanuele Gaifas | comincerò ad aver paura di chi mi copia.
l...@metapensiero.it  |                 -- Fortunato Depero, 1929.

-- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to