Repository: ibis-project/ibis

Issue:
bug: Postgres transactions fail when `_CREATE_FIRST_LAST_AGGS_SQL` can not execute
### What happened?

Transactions with Redshift are failing with a Syntax error, e.g. the `connection.table()`, `connection.list_tables()` methods, etc. While I'm seeing the issue on Redshift it looks like it might be a more general error for Postgres users as well who don't have permissions to create functions as the exception is caught and user warning issue, but it seems like the cursor is not being cleaned up properly.

From what I can gather it seems that after upgrading from v4.xx to 5.1 this PR might be what is causing the error: #5780, specifically the `receive_before_execute` and `receive_after_execute` methods.

If I patch the first last aggs statements to empty strings as follows the code works as expected

```python
with mock.patch('ibis.backends.postgres._CREATE_FIRST_LAST_AGGS_SQL', ''), \
          mock.patch('ibis.backends.postgres._DROP_FIRST_LAST_AGGS_SQL', ''):
    connection.table(...)
```



### What version of ibis are you using?

5.1.0

### What backend(s) are you using, if any?

Postgres as an interface to Redshift

### Relevant log output

```sh
UserWarning: Unable to create first/last aggregates: syntax error at or near "PARALLEL"
LINE 3: LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
                                      ^

  warnings.warn(f"Unable to create first/last aggregates: {e}")
---------------------------------------------------------------------------
InFailedSqlTransaction                    Traceback (most recent call last)
~/virtualenvs/v3.8/lib/python3.8/site-packages/sqlalchemy/engine/base.py in _execute_context(self, dialect, constructor, statement, parameters, execution_options, *args, **kw)
   1904                 if not evt_handled:
-> 1905                     self.dialect.do_execute(
   1906                         cursor, statement, parameters, context

~/virtualenvs/v3.8/lib/python3.8/site-packages/sqlalchemy/engine/default.py in do_execute(self, cursor, statement, parameters, context)
    735     def do_execute(self, cursor, statement, parameters, context=None):
--> 736         cursor.execute(statement, parameters)
    737 

InFailedSqlTransaction: current transaction is aborted, commands ignored until end of transaction block
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
@dantegates Welcome! Thanks for the issue.

Looks like a bug :)

Slated for 6.0.
This is fairly easy to reproduce by intentionally introducing a syntax error into the postgres backend.

Resolving patch:
diff --git a/ibis/backends/postgres/__init__.py b/ibis/backends/postgres/__init__.py
--- a/ibis/backends/postgres/__init__.py
+++ b/ibis/backends/postgres/__init__.py
@@ -2,7 +2,6 @@
 
 from __future__ import annotations
 
-import warnings
 from typing import TYPE_CHECKING, Iterable, Literal
 
 import sqlalchemy as sa
@@ -17,37 +16,8 @@
     import ibis.expr.datatypes as dt
 
 
-# adapted from https://wiki.postgresql.org/wiki/First/last_%28aggregate%29
-_CREATE_FIRST_LAST_AGGS_SQL = """\
-CREATE OR REPLACE FUNCTION public._ibis_first_agg (anyelement, anyelement)
-RETURNS anyelement
-LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
-'SELECT $1';
-
-CREATE OR REPLACE AGGREGATE public._ibis_first (anyelement) (
-  SFUNC = public._ibis_first_agg,
-  STYPE = anyelement,
-  PARALLEL = safe
-);
-
-CREATE OR REPLACE FUNCTION public._ibis_last_agg (anyelement, anyelement)
-RETURNS anyelement
-LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
-'SELECT $2';
-
-CREATE OR REPLACE AGGREGATE public._ibis_last (anyelement) (
-  SFUNC = public._ibis_last_agg,
-  STYPE = anyelement,
-  PARALLEL = safe
-);"""
-
-_DROP_FIRST_LAST_AGGS_SQL = """\
-DROP AGGREGATE IF EXISTS public._ibis_first(anyelement), public._ibis_last(anyelement);
-DROP FUNCTION IF EXISTS public._ibis_first_agg(anyelement, anyelement), public._ibis_last_agg(anyelement, anyelement);"""
-
-
 class Backend(BaseAlchemyBackend):
-    name = 'postgres'
+    name = "postgres"
     compiler = PostgreSQLCompiler
     supports_create_or_replace = False
 
@@ -149,28 +119,6 @@ def connect(dbapi_connection, connection_record):
             with dbapi_connection.cursor() as cur:
                 cur.execute("SET TIMEZONE = UTC")
 
-        @sa.event.listens_for(engine, "before_execute")
-        def receive_before_execute(
-            conn, clauseelement, multiparams, params, execution_options
-        ):
-            with conn.connection.cursor() as cur:
-                try:
-                    cur.execute(_CREATE_FIRST_LAST_AGGS_SQL)
-                except Exception as e:  # noqa: BLE001
-                    # a user may not have permissions to create funtions and/or aggregates
-                    warnings.warn(f"Unable to create first/last aggregates: {e}")
-
-        @sa.event.listens_for(engine, "after_execute")
-        def receive_after_execute(
-            conn, clauseelement, multiparams, params, execution_options, result
-        ):
-            with conn.connection.cursor() as cur:
-                try:
-                    cur.execute(_DROP_FIRST_LAST_AGGS_SQL)
-                except Exception as e:  # noqa: BLE001
-                    # a user may not have permissions to drop funtions and/or aggregates
-                    warnings.warn(f"Unable to drop first/last aggregates: {e}")
-
         super().do_connect(engine)
 
     def list_databases(self, like=None):
diff --git a/ibis/backends/postgres/registry.py b/ibis/backends/postgres/registry.py
--- a/ibis/backends/postgres/registry.py
+++ b/ibis/backends/postgres/registry.py
@@ -496,7 +496,7 @@ def _arbitrary(t, op):
         raise com.UnsupportedOperationError(
             f"postgres backend doesn't support how={how!r} for the arbitrary() aggregate"
         )
-    func = getattr(sa.func.public, f"_ibis_{op.how}")
+    func = getattr(sa.func, op.how)
     return t._reduction(func, op)
 
 
@@ -709,7 +709,7 @@ def _unnest(t, op):
         ops.Arbitrary: _arbitrary,
         ops.StructColumn: _struct_column,
         ops.StructField: _struct_field,
-        ops.First: reduction(sa.func.public._ibis_first),
-        ops.Last: reduction(sa.func.public._ibis_last),
+        ops.First: reduction(sa.func.first),
+        ops.Last: reduction(sa.func.last),
     }
 )


Test patch:
diff --git a/ibis/backends/tests/snapshots/test_sql/test_union_aliasing/postgres/out.sql b/ibis/backends/tests/snapshots/test_sql/test_union_aliasing/postgres/out.sql
--- a/ibis/backends/tests/snapshots/test_sql/test_union_aliasing/postgres/out.sql
+++ b/ibis/backends/tests/snapshots/test_sql/test_union_aliasing/postgres/out.sql
@@ -35,7 +35,7 @@ WITH t0 AS (
 ), t4 AS (
   SELECT
     t3.field_of_study AS field_of_study,
-    public._IBIS_FIRST(t3.diff) AS diff
+    FIRST(t3.diff) AS diff
   FROM t3
   GROUP BY
     1