vonPetrushev
vonPetrushev

Reputation: 5599

VALUES clause in SQLAlchemy

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;

From what I see in the documentation, the VALUES clause appears only in use with INSERT.

Upvotes: 22

Views: 17564

Answers (6)

tommyip
tommyip

Reputation: 450

This is now natively available in SQLAlchemy.

Your example would be written as:

from sqlalchemy import select, column, Integer
from sqlalchemy.sql import Values

select(Values(column('Number', Integer), name='sq').data([(1,), (2,), (3,)]))

There doesn't seem to be any documentation on this, but you can have a look at the test cases https://github.com/sqlalchemy/sqlalchemy/blob/master/test/sql/test_values.py

Upvotes: 24

Daniel Harvey
Daniel Harvey

Reputation: 39

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)

Upvotes: 0

Nikita Malovichko
Nikita Malovichko

Reputation: 131

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.

Upvotes: 1

Alex Isayko
Alex Isayko

Reputation: 322

from sqlalchemy import select, func

select(['*']).select_from(func.values([1, 2, 3, 4]))

Upvotes: 1

zzzeek
zzzeek

Reputation: 75137

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

Upvotes: 15

Sonam
Sonam

Reputation: 3466

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

Upvotes: -3

Related Questions