Skip to content

Add an overload to the exec method with _Executable statement for update and delete statements #909

Description

@joachimhuet

I think we should add an overload to the exec method to still have the possibility of passing an _Executable statement:

    @overload
    def exec(
        self,
        statement: _Executable,
        *,
        params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None,
        execution_options: Mapping[str, Any] = util.EMPTY_DICT,
        bind_arguments: Optional[Dict[str, Any]] = None,
        _parent_execute_state: Optional[Any] = None,
        _add_event: Optional[Any] = None,
    ) -> TupleResult[_TSelectParam]:
        ...

Originally posted by @joachimhuet in #831 (comment)

Activity

  1. federicober commented on May 20, 2024

    @federicober

    This is also valid for insert statements

  2. viaSeunghyun commented on Jul 10, 2024

    @viaSeunghyun

    I agree.

    The exec() method of Session allows Executable as a parameter for the statement, as shown in the code block below.

    Yes, Session's method exec() has Executable as its type.

    class Session(_Session):
        def exec(
            self,
            statement: Union[
                Select[_TSelectParam],
                SelectOfScalar[_TSelectParam],
                Executable[_TSelectParam],            # Here
            ],
            *,
            params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None,
            execution_options: Mapping[str, Any] = util.EMPTY_DICT,
            bind_arguments: Optional[Dict[str, Any]] = None,
            _parent_execute_state: Optional[Any] = None,
            _add_event: Optional[Any] = None,
        ) -> Union[TupleResult[_TSelectParam], ScalarResult[_TSelectParam]]:
    

    When using direct SQL statements, especially when preparing user input, it needs to be wrapped with the text() function.
    The text() returns a TextClause, which inherits from Executable through multiple inheritance.

    To safely perform SQL queries, you need to wrap them in text(),

    @_document_text_coercion("text", ":func:`.text`", ":paramref:`.text.text`")
    def text(text: str) -> TextClause:   # Return Hint is TextClause
    
    class TextClause(
        roles.DDLConstraintColumnRole,
        roles.DDLExpressionRole,
        roles.StatementOptionRole,
        roles.WhereHavingRole,
        roles.OrderByRole,
        roles.FromClauseRole,
        roles.SelectStatementRole,
        roles.InElementRole,
        Generative,
        Executable,                        # TextClause inherits Executable
        DQLDMLClauseElement,
        roles.BinaryElementRole[Any],
        inspection.Inspectable["TextClause"],
    ):
    

    However, the problem arises because Executable is missing from the @overload in Session.
    I also received a deprecated warning and tried to transition, but most situations where I use execute are not Select statements. Therefore, I think the current situation of annotating the existing execute() function with @deprecated and only allowing Select in the exec() function is misleading.

    In other words, at least in the 0.110 version I tested, it's clearly a bad idea to display a @deprecated warning when calling the execute() method.

    If only select statements are allowed, execute and exec should be considered as functions with different characteristics.
    This inconsistency causes issues when working with non-Select SQL statements and limits the functionality of the Session.exec() method. It also creates confusion for users trying to follow best practices and handle deprecation warnings appropriately.

    Steps to Reproduce:

    1. Attempt to use Session.exec() with a non-Select SQL statement wrapped in text().
    2. Observe the error or unexpected behavior.

    Expected Behavior:
    Session.exec() should accept all types of SQL statements wrapped in text(), as TextClause inherits from Executable.

    Actual Behavior:
    Session.exec() fails or produces unexpected results with non-Select SQL statements, despite them being valid Executable objects.

    I've been checking and testing this and it does indeed output an error.

    • Exception Type: AttributeError
    • Exception.e: 'Session' object has no attribute 'exec'

    Additional Notes:
    This issue not only affects current usage but also complicates the transition process for users responding to deprecation warnings. It's important to maintain consistency in function behavior and support for different SQL statement types.

  3. sandangel commented on Jul 17, 2024

    @sandangel

    I run into this issue to. Do we have any update?

  4. viaSeunghyun commented on Jul 22, 2024

    @viaSeunghyun

    @sandangel
    Unofficially, don't use exec, ignore the warning, use execute.
    This is the cleanest way to do it without customising the FastAPI, otherwise a new @overload would need to be added in the new version.

  5. jtseven commented on Aug 13, 2024

    @jtseven

    @sandangel Unofficially, don't use exec, ignore the warning, use execute. This is the cleanest way to do it without customising the FastAPI, otherwise a new @overload would need to be added in the new version.

    But execute is marked as deprecated by the type checker. It says:

    You probably want to use session.exec() instead of session.execute().

    This is the original SQLAlchemy session.execute() method that returns objects of type Row, and that you have to call scalars() to get the model objects.

  6. Blue9 commented on Sep 24, 2024

    @Blue9

    One workaround is to use session.connection().execute(statement). This avoids the deprecation warning by explicitly dropping down to SQLAlchemy and is ultimately what SQLAlchemy's session.execute(statement) uses under the hood (link). This shares the session transaction so you can mix this with other operations, and you will still have to call session.commit() or session.rollback() at the end of your transaction.

    This could also cause the local ORM state to be stale so just be wary to refresh relevant ORM objects if you need to use them. Also, if you need to pass in custom options there are some discrepancies between the Session.execute and Connection.execute APIs.

  7. sandangel commented on Sep 26, 2024

    @sandangel

    I just use the sqlalchemy AsyncSession and use session.scalar or session.scalars instead. We don't need to modify AsyncSession of sqlmodel.

  8. ddahan commented on Oct 12, 2024

    @ddahan

    These workarounds falling back to raw SQLAlchemy instead of SQLModel feel clunky and confusing to me.
    I'm not sure to understand the real reason of why it is impossible to write this code without having a warning?

    def delete_bulk_objects(model: type[T], conditions: list[Any], session: Session):
        statement = delete(model).where(*conditions)
        session.exec(statement)  # without type: ignore, it shows the 'session.exec(statement)' error.
        session.commit()
  9. vanakema commented on Nov 5, 2024

    @vanakema

    I'm kind of confused as to why there's so much discussion about working around this using unwrapped sqlalchemy. Calling exec DOES work in my experience for these situations. It just fails the type checker because it's missing the overload.

    Am I missing something? If not, why isn't it just added as another overload?

  10. chriscarrollsmith commented on Nov 18, 2024

    @chriscarrollsmith

    Can confirm that exec works, but raises mypy errors:

    tests/conftest.py:48: error: No overload variant of "exec" of "Session" matches argument type "Delete"  [call-overload]
    tests/conftest.py:48: note: Possible overload variants:
    tests/conftest.py:48: note:     def [_TSelectParam: Any] exec(self, statement: Select[_TSelectParam], *, params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = ..., execution_options: Mapping[str, Any] = ..., bind_arguments: dict[str, Any] | None = ..., _parent_execute_state: Any | None = ..., _add_event: Any | None = ...) -> TupleResult[_TSelectParam]
    tests/conftest.py:48: note:     def [_TSelectParam: Any] exec(self, statement: SelectOfScalar[_TSelectParam], *, params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = ..., execution_options: Mapping[str, Any] = ..., bind_arguments: dict[str, Any] | None = ..., _parent_execute_state: Any | None = ..., _add_event: Any | None = ...) -> ScalarResult[_TSelectParam]
    tests/conftest.py:49: error: No overload variant of "exec" of "Session" matches argument type "Delete"  [call-overload]
    tests/conftest.py:49: note: Possible overload variants:
    tests/conftest.py:49: note:     def [_TSelectParam: Any] exec(self, statement: Select[_TSelectParam], *, params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = ..., execution_options: Mapping[str, Any] = ..., bind_arguments: dict[str, Any] | None = ..., _parent_execute_state: Any | None = ..., _add_event: Any | None = ...) -> TupleResult[_TSelectParam]
    tests/conftest.py:49: note:     def [_TSelectParam: Any] exec(self, statement: SelectOfScalar[_TSelectParam], *, params: Mapping[str, Any] | Sequence[Mapping[str, Any]] | None = ..., execution_options: Mapping[str, Any] = ..., bind_arguments: dict[str, Any] | None = ..., _parent_execute_state: Any | None = ..., _add_event: Any | None = ...) -> ScalarResult[_TSelectParam]
    Found 2 errors in 1 file (checked 14 source files)
    
  11. mimoo commented on Feb 26, 2025

    @mimoo

    surprising that the doc doesn't have an example on deleting many rows! indeed I can't find a way to do it without a typing error

    https://sqlmodel.tiangolo.com/tutorial/delete/#recap

  12. seriaati commented on Apr 9, 2025

    @seriaati
  13. gaardhus commented on Apr 9, 2025

    @gaardhus
  14. vanakema commented on Apr 9, 2025

    @vanakema
  15. seriaati commented on Apr 10, 2025

    @seriaati
    Contributor

    Sorry, didn't realize that there was another typing error with the statement itself.
    I tried to fix this error in #1342, feedback is welcomed!

  16. 2 remaining items

  17. Aydin-ab commented on Jun 16, 2025

    @Aydin-ab

    Why is it not fixed already, it's just adding an overload, why is it taking more than a year, what is going on ?!

  18. vanakema commented on Jun 17, 2025

    @vanakema

    Why is it not fixed already, it's just adding an overload, why is it taking more than a year, what is going on ?!

    The bystander effect 😁

  19. seriaati commented on Jun 18, 2025

    @seriaati
    Contributor

    Why is it not fixed already, it's just adding an overload, why is it taking more than a year, what is going on ?!

    I had the exact same question when I was looking at this issue. This is why I decided to take action 😅

  20. Aydin-ab commented on Jun 18, 2025

    @Aydin-ab
  21. vanakema commented on Jun 24, 2025

    @vanakema
  22. sglbl commented on Jul 22, 2025

    @sglbl

    Pyright also complains:
    /home/sglbl/sg_project_template/src/infra/postgres/database.py:32:15 -
    error: No overloads for "exec" match the provided arguments (reportCallIssue)

  23. tiangolo commented on Sep 17, 2025

    @tiangolo
    Member

    This should be solved by #1342, available in SQLModel version 0.0.25, released in the next few hours. 🎉

  24. Vigilans commented on Nov 17, 2025

    @Vigilans

    Issue in #376 seems not fixed actually? There is still no overload for text() clause.

  25. putradimas commented on Jun 11, 2026

    @putradimas

    I'm using version 0.0.38 and the overload of exec for text still doesn't exist.

    No overloads for "exec" match the provided arguments - Pylance[reportCallIssue]

  26. YuriiMotov commented on Jun 11, 2026

    @YuriiMotov
    Member

    I'm using version 0.0.38 and the overload of exec for text still doesn't exist.

    See #1657

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions