VALUES clause in SQLAlchemy

后端 未结 6 757
北海茫月
北海茫月 2020-12-01 17:26

Is there a way to build a Query object in SQLAlchemy which will be the equivalent of:

SELECT * FROM (VALUES (1, 2, 3)) AS sq;

相关标签:
6条回答
  • 2020-12-01 17:34

    well "VALUES" in an insert is the standard SQL, the standalone "VALUES" keyword is a Postgresql thing. There's a quick compiler recipe for this one at PGValues (copied here in case I change the wiki someday):

    from sqlalchemy import *
    from sqlalchemy.ext.compiler import compiles
    from sqlalchemy.sql.expression import FromClause
    from sqlalchemy.sql import table, column
    
    class values(FromClause):
        def __init__(self, *args):
            self.list = args
    
        def _populate_column_collection(self):
            self._columns.update(
                [("column%d" % i, column("column%d" % i))
                        for i in xrange(1, len(self.list[0]) + 1)]
            )
    
    @compiles(values)
    def compile_values(element, compiler, asfrom=False, **kw):
        v = "VALUES %s" % ", ".join(
            "(%s)" % ", ".join(compiler.render_literal_value(elem, None) for elem in tup)
            for tup in element.list
        )
        if asfrom:
            v = "(%s)" % v
        return v
    
    if __name__ == '__main__':
        t1 = table('t1', column('a'), column('b'))
        t2 = values((1, 0.5), (2, -0.5)).alias('weights')
        print select([t1, t2]).select_from(t1.join(t2, t1.c.a==t2.c.column2))
    
    0 讨论(0)
  • 2020-12-01 17:34

    I'm not exactly sure but you can try the following options:

    SELECT * FROM (select 1 as col1 union select 2 as col1 union select 3 as col1) AS sq
    
    0 讨论(0)
  • 2020-12-01 17:36
    from sqlalchemy import select, func
    
    select(['*']).select_from(func.values([1, 2, 3, 4]))
    
    0 讨论(0)
  • 2020-12-01 17:43

    Not the best solution but it worked for me:

    import sqlalchemy as sa
    
    query = sa.select(['*']).select_from(sa.text("(VALUES (1,2,3)) as sq"))
    connection.execute(query).fetchall()
    

    Output: [(1, 2, 3)]

    PS: VALUES with the aliased columns example:

    import sqlalchemy as sa
    
    query_cte = (
        sa.select([sa.column('age'), sa.column('name')])
            .select_from(sa.text("(VALUES (22, 'Bob'), (30, 'Julia')) as t (age, name)"))
    ).cte()
    query_name = sa.select([query_cte.c.name])
    connection.execute(query_name).fetchall()
    

    Output: [('Bob',), ('Julia',)]

    WARNING: This solution is good for simple values. Be careful with the special symbols or words, they should be properly escaped.

    0 讨论(0)
  • 2020-12-01 17:46

    I suggest my version of zzzeek's snippet which

    • renders None as NULL
    • specifies literal type based on column type
    • handles ARRAY literals (useful for PostgreSQL)

    How to make use of bindparam() in a custom Compiled expression?

    0 讨论(0)
  • 2020-12-01 17:57

    I went a bit further, to implement SELECT INTO <table> VALUES (...).

    The following implementation is designed to work with PostgreSQL and Python 3, and handles values of differing types (boolean, integer, float, JSON and varchar):

    import json
    from sqlalchemy import exc
    from sqlalchemy.dialects import postgresql
    from sqlalchemy.ext.compiler import compiles
    from sqlalchemy.sql import sqltypes, FromClause, Select
    
    class SelectInto(Select):
        def __init__(self, columns, into, *arg, **kw):
            super(SelectInto, self).__init__(columns, *arg, **kw)
            self.into = into
    
    @compiles(SelectInto)
    def _select_into(element, compiler, **kw):
        text = compiler.visit_select(element, **kw)
        text = text.replace("FROM", f"INTO {element.into} \nFROM")
        return text
    
    class Values(FromClause):
        text_type = sqltypes.UnicodeText
    
        def __init__(self, cols, types=None, *args):
            self.cols = cols
            self.vals = args
    
            if isinstance(self.cols, str):
                self.cols = [c.strip().join('""') for c in self.cols.split(",")]
    
            if not types:
                self.types = [self.text_type for _ in range(len(self.cols))]
            elif len(cols) == len(types):
                self.types = [self._map_col_type(t) for t in types]
            else:
                raise exc.ArgumentError("Types do not correspond to columns")
    
        def _map_col_type(self, col_type):
            if isinstance(col_type, sqltypes.TypeEngine):
                return col_type
    
            col_type = col_type.lower()
    
            if col_type in ("bool", "boolean"):
                return sqltypes.Boolean
            elif col_type in ("int", "integer", "number"):
                return sqltypes.Integer
            elif col_type in ("float", "double"):
                return sqltypes.Float
            elif col_type in ("json",):
                return postgresql.json.JSON
            elif col_type in ("jsonb",):
                return postgresql.json.JSONB
    
            return self.text_type
    
    @compiles(Values)
    def _compile_values(element, compiler, **kw):
        value_cols = ",".join(element.cols)
        value_sets = ", ".join(
            "({values})".format(
                values=",".join(_compile_value(compiler, val, element.types[idx]) for idx, val in enumerate(tup))
            )
            for tup in element.vals
        )
        return f'(VALUES {value_sets}) AS "values" ({value_cols})'
    
    def _compile_value(compiler, value, type_):
        if value is None:
            return "NULL"
        elif issubclass(type_, sqltypes.JSON):
            if isinstance(value, dict):
                value = json.dumps(value)
            return f"'{value}'::{type_.__name__}"
    
        if issubclass(type_, sqltypes.String):
            value = str(value)
    
        return compiler.render_literal_value(value, type_())
    

    To test this:

    from sqlalchemy.sql.expression column
    
    select_cols = [column(c) for c in 'one,two,three'.split(',')]
    select_from = Values(['one', 'two', 'three'], ['varchar', 'int', 'bool'], *(('a',1,0),('aa',11,1),('aaa',111,0)))
    select_into = SelectInto(select_cols, 'test_select').select_from(select_from)
    print(select_into)
    
    0 讨论(0)
提交回复
热议问题