Repository: ibis-project/ibis

Issue:
bug Error when connecting to Trino after upgrading to 9.0.0
### What happened?

In version 8.0.0, the following code works fine:

```
import ibis
from dotenv import load_dotenv
import os


load_dotenv()


catalog = os.getenv('TRINO_CATALOG')
schema = os.getenv('TRINO_SCHEMA')
user = os.getenv('TRINO_USER')
password = os.getenv('TRINO_PASSWORD')
trino_url = os.getenv('TRINO_URL')

con = ibis.connect(f"trino://{user}:{password}@{trino_url}/{catalog}/{schema}")

df_history_tag = con.table('history_tag_4')
```

After upgrading to 9.0.0, executing the above code results in the following error:

```
{
	"name": "TrinoConnectionError",
	"message": "failed to execute: HTTPConnectionPool(host='localhost', port=8080): Max retries exceeded with url: /v1/statement (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f3e2454b2d0>: Failed to establish a new connection: [Errno 111] Connection refused'))",
	"stack": "---------------------------------------------------------------------------
ConnectionRefusedError                    Traceback (most recent call last)
File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/urllib3/connection.py:198, in HTTPConnection._new_conn(self)
    197 try:
--> 198     sock = connection.create_connection(
    199         (self._dns_host, self.port),
    200         self.timeout,
    201         source_address=self.source_address,
    202         socket_options=self.socket_options,
    203     )
    204 except socket.gaierror as e:

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/urllib3/util/connection.py:85, in create_connection(address, timeout, source_address, socket_options)
     84 try:
---> 85     raise err
     86 finally:
     87     # Break explicitly a reference cycle

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/urllib3/util/connection.py:73, in create_connection(address, timeout, source_address, socket_options)
     72     sock.bind(source_address)
---> 73 sock.connect(sa)
     74 # Break explicitly a reference cycle

ConnectionRefusedError: [Errno 111] Connection refused

The above exception was the direct cause of the following exception:

NewConnectionError                        Traceback (most recent call last)
File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/urllib3/connectionpool.py:793, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    792 # Make the request on the HTTPConnection object
--> 793 response = self._make_request(
    794     conn,
    795     method,
    796     url,
    797     timeout=timeout_obj,
    798     body=body,
    799     headers=headers,
    800     chunked=chunked,
    801     retries=retries,
    802     response_conn=response_conn,
    803     preload_content=preload_content,
    804     decode_content=decode_content,
    805     **response_kw,
    806 )
    808 # Everything went great!

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/urllib3/connectionpool.py:496, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
    495 try:
--> 496     conn.request(
    497         method,
    498         url,
    499         body=body,
    500         headers=headers,
    501         chunked=chunked,
    502         preload_content=preload_content,
    503         decode_content=decode_content,
    504         enforce_content_length=enforce_content_length,
    505     )
    507 # We are swallowing BrokenPipeError (errno.EPIPE) since the server is
    508 # legitimately able to close the connection after sending a valid response.
    509 # With this behaviour, the received response is still readable.

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/urllib3/connection.py:400, in HTTPConnection.request(self, method, url, body, headers, chunked, preload_content, decode_content, enforce_content_length)
    399     self.putheader(header, value)
--> 400 self.endheaders()
    402 # If we're given a body we start sending that in chunks.

File ~/.pyenv/versions/3.11.7/lib/python3.11/http/client.py:1289, in HTTPConnection.endheaders(self, message_body, encode_chunked)
   1288     raise CannotSendHeader()
-> 1289 self._send_output(message_body, encode_chunked=encode_chunked)

File ~/.pyenv/versions/3.11.7/lib/python3.11/http/client.py:1048, in HTTPConnection._send_output(self, message_body, encode_chunked)
   1047 del self._buffer[:]
-> 1048 self.send(msg)
   1050 if message_body is not None:
   1051 
   1052     # create a consistent interface to message_body

File ~/.pyenv/versions/3.11.7/lib/python3.11/http/client.py:986, in HTTPConnection.send(self, data)
    985 if self.auto_open:
--> 986     self.connect()
    987 else:

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/urllib3/connection.py:238, in HTTPConnection.connect(self)
    237 def connect(self) -> None:
--> 238     self.sock = self._new_conn()
    239     if self._tunnel_host:
    240         # If we're tunneling it means we're connected to our proxy.

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/urllib3/connection.py:213, in HTTPConnection._new_conn(self)
    212 except OSError as e:
--> 213     raise NewConnectionError(
    214         self, f\"Failed to establish a new connection: {e}\"
    215     ) from e
    217 # Audit hooks are only available in Python 3.8+

NewConnectionError: <urllib3.connection.HTTPConnection object at 0x7f3e2454b2d0>: Failed to establish a new connection: [Errno 111] Connection refused

The above exception was the direct cause of the following exception:

MaxRetryError                             Traceback (most recent call last)
File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/requests/adapters.py:486, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    485 try:
--> 486     resp = conn.urlopen(
    487         method=request.method,
    488         url=url,
    489         body=request.body,
    490         headers=request.headers,
    491         redirect=False,
    492         assert_same_host=False,
    493         preload_content=False,
    494         decode_content=False,
    495         retries=self.max_retries,
    496         timeout=timeout,
    497         chunked=chunked,
    498     )
    500 except (ProtocolError, OSError) as err:

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/urllib3/connectionpool.py:847, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    845     new_e = ProtocolError(\"Connection aborted.\", new_e)
--> 847 retries = retries.increment(
    848     method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2]
    849 )
    850 retries.sleep()

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/urllib3/util/retry.py:515, in Retry.increment(self, method, url, response, error, _pool, _stacktrace)
    514     reason = error or ResponseError(cause)
--> 515     raise MaxRetryError(_pool, url, reason) from reason  # type: ignore[arg-type]
    517 log.debug(\"Incremented Retry for (url='%s'): %r\", url, new_retry)

MaxRetryError: HTTPConnectionPool(host='localhost', port=8080): Max retries exceeded with url: /v1/statement (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f3e2454b2d0>: Failed to establish a new connection: [Errno 111] Connection refused'))

During handling of the above exception, another exception occurred:

ConnectionError                           Traceback (most recent call last)
File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/trino/client.py:793, in TrinoQuery.execute(self, additional_http_headers)
    792 try:
--> 793     response = self._request.post(self._query, additional_http_headers)
    794 except requests.exceptions.RequestException as e:

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/trino/client.py:555, in TrinoRequest.post(self, sql, additional_http_headers)
    553 http_headers.update(additional_http_headers or {})
--> 555 http_response = self._post(
    556     self.statement_url,
    557     data=data,
    558     headers=http_headers,
    559     timeout=self._request_timeout,
    560     proxies=PROXIES,
    561 )
    562 return http_response

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/trino/client.py:894, in _retry_with.<locals>.wrapper.<locals>.decorated(*args, **kwargs)
    893 if error is not None:
--> 894     raise error
    895 return result

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/trino/client.py:881, in _retry_with.<locals>.wrapper.<locals>.decorated(*args, **kwargs)
    880 try:
--> 881     result = func(*args, **kwargs)
    882     if any(guard(result) for guard in conditions):

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/requests/sessions.py:637, in Session.post(self, url, data, json, **kwargs)
    627 r\"\"\"Sends a POST request. Returns :class:`Response` object.
    628 
    629 :param url: URL for the new :class:`Request` object.
   (...)
    634 :rtype: requests.Response
    635 \"\"\"
--> 637 return self.request(\"POST\", url, data=data, json=json, **kwargs)

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/requests/sessions.py:589, in Session.request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    588 send_kwargs.update(settings)
--> 589 resp = self.send(prep, **send_kwargs)
    591 return resp

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/requests/sessions.py:703, in Session.send(self, request, **kwargs)
    702 # Send the request
--> 703 r = adapter.send(request, **kwargs)
    705 # Total elapsed time of the request (approximately)

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/requests/adapters.py:519, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    517         raise SSLError(e, request=request)
--> 519     raise ConnectionError(e, request=request)
    521 except ClosedPoolError as e:

ConnectionError: HTTPConnectionPool(host='localhost', port=8080): Max retries exceeded with url: /v1/statement (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f3e2454b2d0>: Failed to establish a new connection: [Errno 111] Connection refused'))

During handling of the above exception, another exception occurred:

TrinoConnectionError                      Traceback (most recent call last)
Cell In[3], line 1
----> 1 df_history_tag = con.table('history_tag_4')

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/ibis/backends/sql/__init__.py:137, in SQLBackend.table(self, name, schema, database)
    134     catalog = table_loc.catalog or None
    135     database = table_loc.db or None
--> 137 table_schema = self.get_schema(name, catalog=catalog, database=database)
    138 return ops.DatabaseTable(
    139     name,
    140     schema=table_schema,
    141     source=self,
    142     namespace=ops.Namespace(catalog=catalog, database=database),
    143 ).to_expr()

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/ibis/backends/trino/__init__.py:141, in Backend.get_schema(self, table_name, catalog, database)
    128     conditions.append(sg.column(\"table_schema\").eq(sge.convert(database)))
    130 query = (
    131     sg.select(
    132         \"column_name\",
   (...)
    138     .order_by(\"ordinal_position\")
    139 )
--> 141 with self._safe_raw_sql(query) as cur:
    142     meta = cur.fetchall()
    144 if not meta:

File ~/.pyenv/versions/3.11.7/lib/python3.11/contextlib.py:137, in _GeneratorContextManager.__enter__(self)
    135 del self.args, self.kwds, self.func
    136 try:
--> 137     return next(self.gen)
    138 except StopIteration:
    139     raise RuntimeError(\"generator didn't yield\") from None

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/ibis/backends/trino/__init__.py:93, in Backend._safe_raw_sql(self, query)
     76 @contextlib.contextmanager
     77 def _safe_raw_sql(
     78     self, query: str | sge.Expression
     79 ) -> Iterator[trino.dbapi.Cursor]:
     80     \"\"\"Execute a raw SQL query, yielding the cursor.
     81 
     82     Parameters
   (...)
     91 
     92     \"\"\"
---> 93     cur = self.raw_sql(query)
     94     try:
     95         yield cur

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/ibis/backends/trino/__init__.py:47, in Backend.raw_sql(self, query)
     45 cur = con.cursor()
     46 try:
---> 47     cur.execute(query)
     48 except Exception:
     49     if con.transaction is not None:

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/trino/dbapi.py:592, in Cursor.execute(self, operation, params)
    589 else:
    590     self._query = trino.client.TrinoQuery(self._request, query=operation,
    591                                           legacy_primitive_types=self._legacy_primitive_types)
--> 592     self._iterator = iter(self._query.execute())
    593 return self

File ~/.pyenv/versions/3.11.7/envs/venv311/lib/python3.11/site-packages/trino/client.py:795, in TrinoQuery.execute(self, additional_http_headers)
    793     response = self._request.post(self._query, additional_http_headers)
    794 except requests.exceptions.RequestException as e:
--> 795     raise trino.exceptions.TrinoConnectionError(\"failed to execute: {}\".format(e))
    796 status = self._request.process(response)
    797 self._info_uri = status.info_uri

TrinoConnectionError: failed to execute: HTTPConnectionPool(host='localhost', port=8080): Max retries exceeded with url: /v1/statement (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f3e2454b2d0>: Failed to establish a new connection: [Errno 111] Connection refused'))"
}
```

### What version of ibis are you using?

9.0.0

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

trino

### Relevant log output

_No response_

### Code of Conduct

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


Discussion:

Resolving patch:
diff --git a/ibis/backends/trino/__init__.py b/ibis/backends/trino/__init__.py
--- a/ibis/backends/trino/__init__.py
+++ b/ibis/backends/trino/__init__.py
@@ -6,6 +6,7 @@
 from functools import cached_property
 from operator import itemgetter
 from typing import TYPE_CHECKING, Any
+from urllib.parse import urlparse
 
 import sqlglot as sg
 import sqlglot.expressions as sge
@@ -16,7 +17,7 @@
 import ibis.expr.schema as sch
 import ibis.expr.types as ir
 from ibis import util
-from ibis.backends import CanCreateDatabase, CanCreateSchema, CanListCatalog, NoUrl
+from ibis.backends import CanCreateDatabase, CanCreateSchema, CanListCatalog
 from ibis.backends.sql import SQLBackend
 from ibis.backends.sql.compiler import C
 from ibis.backends.trino.compiler import TrinoCompiler
@@ -30,12 +31,25 @@
     import ibis.expr.operations as ops
 
 
-class Backend(SQLBackend, CanListCatalog, CanCreateDatabase, CanCreateSchema, NoUrl):
+class Backend(SQLBackend, CanListCatalog, CanCreateDatabase, CanCreateSchema):
     name = "trino"
     compiler = TrinoCompiler()
     supports_create_or_replace = False
     supports_temporary_tables = False
 
+    def _from_url(self, url: str, **kwargs):
+        url = urlparse(url)
+        catalog, db = url.path.strip("/").split("/")
+        self.do_connect(
+            user=url.username or None,
+            password=url.password or None,
+            host=url.hostname or None,
+            port=url.port or None,
+            database=catalog,
+            schema=db,
+        )
+        return self
+
     def raw_sql(self, query: str | sg.Expression) -> Any:
         """Execute a raw SQL query."""
         with contextlib.suppress(AttributeError):


Test patch:
diff --git a/ibis/backends/trino/tests/test_client.py b/ibis/backends/trino/tests/test_client.py
--- a/ibis/backends/trino/tests/test_client.py
+++ b/ibis/backends/trino/tests/test_client.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import math
+import os
 import string
 
 import pytest
@@ -183,3 +184,15 @@ def test_list_tables_schema_warning_refactor(con):
 
     assert con.list_tables(database="tpch.sf1") == tpch_tables
     assert con.list_tables(database=("tpch", "sf1")) == tpch_tables
+
+
+def test_connect_uri():
+    TRINO_USER = os.getenv("IBIS_TEST_TRINO_USER", os.getenv("TRINO_USER", "user"))
+    TRINO_PASS = os.getenv("IBIS_TEST_TRINO_PASSWORD", os.getenv("TRINO_PASSWORD", ""))
+    TRINO_HOST = os.getenv("IBIS_TEST_TRINO_HOST", os.getenv("TRINO_HOST", "localhost"))
+    TRINO_PORT = int(os.getenv("IBIS_TEST_TRINO_PORT", os.getenv("TRINO_PORT", "8080")))
+    con = ibis.connect(
+        f"trino://{TRINO_USER}:{TRINO_PASS}@{TRINO_HOST}:{TRINO_PORT}/memory/default"
+    )
+
+    assert con.list_tables()