On 7/12/15 6:35 AM, Lele Gaifax wrote:
Hi all,

I'm writing a new application against an existing DB (too bad, it's MySQL :-[)
that may contain information in multiple languages. The original author
structured the tables like the following:

   CREATE TABLE products (
     ID integer,
     CODE varchar(20),
/* "short" description */
     IT varchar(100),
     EN varchar(100),
     FR varchar(100),
     ...

     /* "long" description */
     IT_NOTE varchar(500),
     EN_NOTE varchar(500),
     FR_NOTE varchar(500),
     ...
   )

The user of the application has a "preferred language", that can come either
from his own "profile" or from an external source (for example, the browser
locale setting when the user is anonymous).

I would like to be able to express my SA queries with that in mind, so for
example end up with

   SELECT id, code, it as description, it_note as summary
   FROM products

when the "preferred language" is "IT" and with

   SELECT id, code, en as description, en_note as summary
   FROM products

when it is set to "EN".

For now, I settled to build my query using an helper function similar to the
following:

   def localized_field(cols, field_pattern="{lang}"):
       known_langs = ('it', 'en', ...)
       lparam = sa.bindparam('lang')
       return sa.case([(lparam == lang.upper(), 
cols[field_pattern.format(lang=lang)])
                       for lang in known_langs],
                      else_='Unsupported user lang: '+lparam)

that lets me write

   pc = products.c
   query = sqlalchemy.select([pc.id,
                              localized_field(pc).label('description'),
                              localized_field(pc, 
'{lang}_note').label('summary)])

Although it works, it is obviously suboptimal, in particular when I'm going to
further refine the query, for example adding a filter on one of those
fields...

So I wonder if there is any clever trick that SA could support to avoid that
"long" CASE statement for each field. Assuming for example I could "mark" in
some way those columns, I would have a function that, traversing the query
structure replaces all instances of marked fields with the right "LANG_xxx"
one.

So yes, to make this better you'd need to decide on a system to transform the statement as given.

Additionally, you'd need to decide on the hook to use in order to send the transformation instruction into the query and how that would communicate with your outer application. I'm not sure if you're using the ORM, there are some neat new hooks in 1.0 that could be used for this, but let's assume you're doing pure Core.

In this particular case there are a lot of ways to go even if we're just on straight Core. The custom SQL construct approach would possibly be a good way to do this in fact, the construct at @compiles time would swap in the correct column for the render.

Though the approach of traversing the query structure has the advantage that your select() object would now be set up with the correct columns ahead of time and also the "hook" is just a simple "run this function on the statement", rather than adding clues to the execution context.

Since you asked for that, let's assume your hook is just a straight transform function you're going to call on your select() before it is executed. You might be aware that we have the whole "traversal" system. While this system tends to be oriented towards the internals of SQLAlchemy, where in the ORM it is used ubiquitously, it is pretty simple to use in this case, where we can set up a "marker" expression object and use that to indicate where the replacement should occur.

We can just use the '.en' field as the "marker" here, if that works for you. Below I make use of column.shares_lineage() so that we can also accommodate aliases of the products table.

from sqlalchemy import Table, Column, Integer, String, MetaData, select
from sqlalchemy import sql

m = MetaData()
products = Table(
    'products',
    m,
    Column('id', Integer),
    Column('en', String),
    Column('it', String),
    Column('fr', String),

    Column('en_note', String),
    Column('it_note', String),
    Column('fr_note', String),

)


def replace_lang(stmt, lang):
    def replace(obj):
        if isinstance(obj, Column):
            if obj.shares_lineage(products.c.en):
                return obj.table.c[lang]
            elif obj.shares_lineage(products.c.en_note):
                return obj.table.c['%s_note' % lang]
        return None

    return sql.visitors.replacement_traverse(stmt, {}, replace)


pc = products.c

stmt = select([
    pc.id, pc.en.label('description'), pc.en_note.label('summary')
])

print replace_lang(stmt, 'it')


pca = products.alias().c
stmt = select([
    pca.en.label('description'), pca.en_note.label('summary')
])

print replace_lang(stmt, 'it')



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