sqlalchemy icon indicating copy to clipboard operation
sqlalchemy copied to clipboard

convert insert() at ORM level to interpret values() in an ORM context

Open mkmoisen opened this issue 2 years ago • 7 comments

Describe the bug

If the ORM model uses different column names than the Database:

first_name = Column(String(30), name='firstname')

Using the 2.0 style insert() will fail when values() is passed an ORM field name as the keyword. It works when using the DB column names.

Interestingly, update() is the opposite: it is successful when values() is passed an ORM field name as the keyword. It fails when using the DB column names.


update() is correct, the values function should be able to receive the ORM field names, not the DB column names.

To Reproduce

from sqlalchemy.orm import Session, registry, declarative_base, sessionmaker, column_property
from sqlalchemy import create_engine, Integer, String, Column
from sqlalchemy import text, update, select, insert, delete

engine = create_engine("postgresql+psycopg2://test:test@localhost:5432/test", echo=True, future=True)

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    first_name = Column(String(30), name='firstname')
    last_name = Column(String(30), name='lastname')

Base.metadata.create_all(engine, checkfirst=False)

Session = sessionmaker(engine)

session = Session()

# this works fine using ORM field names:
u = User(first_name='foo', last_name='bar')
session.add(u)
session.flush()

# this insert().values() fails with ORM names (CompileError: Unconsumed column names: last_name, first_name):
session.execute(insert(User).values(first_name='foo', last_name='bar'))

# this insert().values() succeeds with DB names:
res = session.execute(insert(User).values(firstname='foo', lastname='bar'))
assert res.rowcount == 1

# this update().values() succeeds with ORM names
res = session.execute(update(User).values(first_name='foo2').where(User.id == u.id))
assert res.rowcount == 1

# this update().values() fails correctly with the DB names (InvalidRequestError: Invalid expression type: 'firstname'):
session.execute(update(User).values(firstname='foo3').where(User.id == u.id))


### Error

Traceback (most recent call last): File "", line 1, in File "C:\test\lib\site-packages\sqlalchemy\orm\session.py", line 1692, in execute result = conn._execute_20(statement, params or {}, execution_options) File "C:\test\lib\site-packages\sqlalchemy\engine\base.py", line 1620, in _execute_20 return meth(self, args_10style, kwargs_10style, execution_options) File "C:\test\lib\site-packages\sqlalchemy\sql\elements.py", line 325, in _execute_on_connection return connection._execute_clauseelement( File "C:\test\lib\site-packages\sqlalchemy\engine\base.py", line 1479, in _execute_clauseelement compiled_sql, extracted_params, cache_hit = elem._compile_w_cache( File "C:\test\lib\site-packages\sqlalchemy\sql\elements.py", line 523, in _compile_w_cache compiled_sql = self._compiler( File "C:\test\lib\site-packages\sqlalchemy\sql\elements.py", line 558, in _compiler return dialect.statement_compiler(dialect, self, **kw) File "C:\test\lib\site-packages\sqlalchemy\sql\compiler.py", line 776, in init Compiled.init(self, dialect, statement, **kwargs) File "C:\test\lib\site-packages\sqlalchemy\sql\compiler.py", line 451, in init self.string = self.process(self.statement, **compile_kwargs) File "C:\test\lib\site-packages\sqlalchemy\sql\compiler.py", line 486, in process return obj._compiler_dispatch(self, **kwargs) File "C:\test\lib\site-packages\sqlalchemy\sql\visitors.py", line 82, in _compiler_dispatch return meth(self, **kw) File "C:\test\lib\site-packages\sqlalchemy\sql\compiler.py", line 3833, in visit_insert crud_params = crud._get_crud_params( File "C:\test\lib\site-packages\sqlalchemy\sql\crud.py", line 168, in _get_crud_params raise exc.CompileError( sqlalchemy.exc.CompileError: Unconsumed column names: last_name, first_name


### Versions

- OS: Windows 10
- Python: 3.9.5
- SQLAlchemy: 1.4.32
- Database: postgresql
- DBAPI (eg: psycopg, cx_oracle, mysqlclient): 1.4.32


### Additional context

_No response_

mkmoisen avatar Mar 28 '22 16:03 mkmoisen

insert is not ORM enabled right now, so this behavior is expected.

since we are pushing for insert() to have more ORM awareness, this can be for 2.0

zzzeek avatar Mar 28 '22 17:03 zzzeek

Hi @zzzeek

Do you know a workaround in 1.4 to use insert().values() with different ORM field names vs database column names?

My library function basically receives a model and a **kwargs like:

def foo(model, **kwargs):
    ins = insert(model).values(**kwargs)

I am using model.__mapper__.attrs to convert before calling insert, like the following:

def foo(model, **kwargs):
    kwargs = {
        model.__mapper__.attrs[orm_field_name].columns[0].name: value
        for orm_field_name, value in kwargs.items()
        if (
            orm_field_name in model.__mapper__.attrs
            and len(model.__mapper__.attrs[orm_field_name].columns) == 1
            and isinstance(model.__mapper__.attrs[orm_field_name].columns[0], Column)
        )
    }

   ins = insert(model).values(**kwargs)
   ...
    

Is there a better way to do it?

insert is not ORM enabled right now

I have been using insert and ORM successfully. This is the only issue I have come across so far.

Is there anything else I should be aware of?

Thank you.

mkmoisen avatar Mar 28 '22 18:03 mkmoisen

Hi @zzzeek

Do you know a workaround in 1.4 to use insert().values() with different ORM field names vs database column names?

My library function basically receives a model and a **kwargs like:

def foo(model, **kwargs):
    ins = insert(model).values(**kwargs)

use the attributes themselves, which represent column expressions, as keys:

 insert(model).values({getattr(model, key): value for key, value in kwargs.items()})

zzzeek avatar Mar 28 '22 18:03 zzzeek

I have been using insert and ORM successfully. This is the only issue I have come across so far.

Is there anything else I should be aware of?

sure, as noted elsewhere, returning(Model) won't return model instances, and if your statement also includes ON CONFLICT types of structures, those UPDATEs wont be reflected in the state of already-loaded objects unless you use the from_statement() workaround in the docs.

zzzeek avatar Mar 28 '22 18:03 zzzeek

there are also no "ORM-enabled" event hooks for session.execute(insert()).

zzzeek avatar Mar 28 '22 18:03 zzzeek

insert(model).values({getattr(model, key): value for key, value in kwargs.items()})

Much more simple than mine; thanks.

mkmoisen avatar Mar 28 '22 21:03 mkmoisen

Mike Bayer has proposed a fix for this issue in the main branch:

ORM bulk insert via execute (WIP, initial reorg) https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/4029

sqla-tester avatar Aug 07 '22 16:08 sqla-tester

@zzzeek Thanks!

mkmoisen avatar Sep 26 '22 15:09 mkmoisen

feel free to try it out, I expect issues are very likely

zzzeek avatar Sep 26 '22 15:09 zzzeek

@zzzeek @mkmoisen Following up on this. Is it possible to either

  • Get "ORM-enabled" event hooks for session.execute(insert()).
  • Use values set by a model, removing the need to use insert.values?

risoms avatar Feb 28 '23 00:02 risoms

@zzzeek @mkmoisen Following up on this. Is it possible to either

* Get "ORM-enabled" event hooks for session.execute(insert()).

do_orm_execute

* Use values set by a model, removing the need to use insert.values?

why not just use session.add_all() ? There is also bulk_save_objects but there's not any huge advantage to that method vs. add_all(), hence it's legacy

zzzeek avatar Feb 28 '23 01:02 zzzeek

@zzzeek @mkmoisen Following up on this. Is it possible to either

* Get "ORM-enabled" event hooks for session.execute(insert()).

do_orm_execute

* Use values set by a model, removing the need to use insert.values?

why not just use session.add_all() ? There is also bulk_save_objects but there's not any huge advantage to that method vs. add_all(), hence it's legacy

@zzzeek This is to support a single insert (upsert) call. Would do_orm_execute() cover this?

risoms avatar Feb 28 '23 02:02 risoms

yes do_orm_execute() does upserts.

zzzeek avatar Feb 28 '23 13:02 zzzeek