Skip to content

Commit

Permalink
Use psycopg rather than psycopg2 for Postgres
Browse files Browse the repository at this point in the history
  • Loading branch information
judahrand committed Jan 8, 2025
1 parent b6e9ef1 commit 7f1292b
Show file tree
Hide file tree
Showing 19 changed files with 100 additions and 56 deletions.
4 changes: 2 additions & 2 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
},
{
"addLabels": ["postgres"],
"matchPackageNames": ["/psycopg2/", "/postgres/"]
"matchPackageNames": ["/psycopg/", "/postgres/"]
},
{
"addLabels": ["druid"],
Expand All @@ -89,7 +89,7 @@
},
{
"addLabels": ["risingwave"],
"matchPackageNames": ["/risingwave/"]
"matchPackageNames": ["/psycopg2/", "/risingwave/"]
},
{
"addLabels": ["snowflake"],
Expand Down
2 changes: 1 addition & 1 deletion conda/environment-arm64-flink.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dependencies:
- pins >=0.8.2
- uv>=0.4.29
- polars >=1,<2
- psycopg2 >=2.8.4
- psycopg >= 3.2.0
- pyarrow =11.0.0
- pyarrow-tests
- pyarrow-hotfix >=0.4
Expand Down
2 changes: 1 addition & 1 deletion conda/environment-arm64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dependencies:
- pins >=0.8.2
- uv>=0.4.29
- polars >=1,<2
- psycopg2 >=2.8.4
- psycopg >= 3.2.0
- pyarrow >=10.0.1
- pyarrow-tests
- pyarrow-hotfix >=0.4
Expand Down
2 changes: 1 addition & 1 deletion conda/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dependencies:
- pip
- uv>=0.4.29
- polars >=1,<2
- psycopg2 >=2.8.4
- psycopg >= 3.2.0
- pyarrow >=10.0.1
- pyarrow-hotfix >=0.4
- pydata-google-auth
Expand Down
43 changes: 21 additions & 22 deletions ibis/backends/postgres/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

import pandas as pd
import polars as pl
import psycopg2
import psycopg
import pyarrow as pa


Expand Down Expand Up @@ -90,8 +90,6 @@ def _from_url(self, url: ParseResult, **kwargs):
return self.connect(**kwargs)

def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
from psycopg2.extras import execute_batch

schema = op.schema
if null_columns := [col for col, dtype in schema.items() if dtype.is_null()]:
raise exc.IbisTypeError(
Expand Down Expand Up @@ -129,7 +127,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:

with self.begin() as cur:
cur.execute(create_stmt_sql)
execute_batch(cur, sql, data, 128)
cur.executemany(sql, data)

Check warning on line 130 in ibis/backends/postgres/__init__.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/__init__.py#L130

Added line #L130 was not covered by tests

@contextlib.contextmanager
def begin(self):
Expand All @@ -145,14 +143,16 @@ def begin(self):
finally:
cursor.close()

def _fetch_from_cursor(self, cursor, schema: sch.Schema) -> pd.DataFrame:
def _fetch_from_cursor(
self, cursor: psycopg.Cursor, schema: sch.Schema
) -> pd.DataFrame:
import pandas as pd

from ibis.backends.postgres.converter import PostgresPandasData

try:
df = pd.DataFrame.from_records(
cursor, columns=schema.names, coerce_float=True
cursor.fetchall(), columns=schema.names, coerce_float=True
)
except Exception:
# clean up the cursor if we fail to create the DataFrame
Expand All @@ -166,7 +166,7 @@ def _fetch_from_cursor(self, cursor, schema: sch.Schema) -> pd.DataFrame:

@property
def version(self):
version = f"{self.con.server_version:0>6}"
version = f"{self.con.info.server_version:0>6}"

Check warning on line 169 in ibis/backends/postgres/__init__.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/__init__.py#L169

Added line #L169 was not covered by tests
major = int(version[:2])
minor = int(version[2:4])
patch = int(version[4:])
Expand Down Expand Up @@ -233,17 +233,17 @@ def do_connect(
year int32
month int32
"""
import psycopg2
import psycopg2.extras
import psycopg
import psycopg.types.json

Check warning on line 237 in ibis/backends/postgres/__init__.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/__init__.py#L236-L237

Added lines #L236 - L237 were not covered by tests

psycopg2.extras.register_default_json(loads=lambda x: x)
psycopg.types.json.set_json_loads(loads=lambda x: x)

Check warning on line 239 in ibis/backends/postgres/__init__.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/__init__.py#L239

Added line #L239 was not covered by tests

self.con = psycopg2.connect(
self.con = psycopg.connect(

Check warning on line 241 in ibis/backends/postgres/__init__.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/__init__.py#L241

Added line #L241 was not covered by tests
host=host,
port=port,
user=user,
password=password,
database=database,
dbname=database,
options=(f"-csearch_path={schema}" * (schema is not None)) or None,
**kwargs,
)
Expand All @@ -252,7 +252,7 @@ def do_connect(

@util.experimental
@classmethod
def from_connection(cls, con: psycopg2.extensions.connection) -> Backend:
def from_connection(cls, con: psycopg.Connection) -> Backend:
"""Create an Ibis client from an existing connection to a PostgreSQL database.
Parameters
Expand Down Expand Up @@ -701,8 +701,9 @@ def _safe_raw_sql(self, *args, **kwargs):
yield result

def raw_sql(self, query: str | sg.Expression, **kwargs: Any) -> Any:
import psycopg2
import psycopg2.extras
import psycopg
import psycopg.types
import psycopg.types.hstore

Check warning on line 706 in ibis/backends/postgres/__init__.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/__init__.py#L704-L706

Added lines #L704 - L706 were not covered by tests

with contextlib.suppress(AttributeError):
query = query.sql(dialect=self.dialect)
Expand All @@ -711,13 +712,11 @@ def raw_sql(self, query: str | sg.Expression, **kwargs: Any) -> Any:
cursor = con.cursor()

try:
# try to load hstore, uuid and ipaddress extensions
with contextlib.suppress(psycopg2.ProgrammingError):
psycopg2.extras.register_hstore(cursor)
with contextlib.suppress(psycopg2.ProgrammingError):
psycopg2.extras.register_uuid(conn_or_curs=cursor)
with contextlib.suppress(psycopg2.ProgrammingError):
psycopg2.extras.register_ipaddress(cursor)
# try to load hstore
with contextlib.suppress(TypeError):
type_info = psycopg.types.TypeInfo.fetch(con, "hstore")
with contextlib.suppress(psycopg.ProgrammingError, TypeError):
psycopg.types.hstore.register_hstore(type_info, cursor)

Check warning on line 719 in ibis/backends/postgres/__init__.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/__init__.py#L716-L719

Added lines #L716 - L719 were not covered by tests
except Exception:
cursor.close()
raise
Expand Down
2 changes: 1 addition & 1 deletion ibis/backends/postgres/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class TestConf(ServiceBackendTest):
supports_structs = False
rounding_method = "half_to_even"
service_name = "postgres"
deps = ("psycopg2",)
deps = ("psycopg",)

Check warning on line 51 in ibis/backends/postgres/tests/conftest.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/tests/conftest.py#L51

Added line #L51 was not covered by tests

driver_supports_multiple_statements = True

Expand Down
8 changes: 4 additions & 4 deletions ibis/backends/postgres/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
import ibis.common.exceptions as com
import ibis.expr.datatypes as dt
import ibis.expr.types as ir
from ibis.backends.tests.errors import PsycoPg2OperationalError
from ibis.backends.tests.errors import PsycoPgOperationalError

Check warning on line 33 in ibis/backends/postgres/tests/test_client.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/tests/test_client.py#L33

Added line #L33 was not covered by tests
from ibis.util import gen_name

pytest.importorskip("psycopg2")
pytest.importorskip("psycopg")

Check warning on line 36 in ibis/backends/postgres/tests/test_client.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/tests/test_client.py#L36

Added line #L36 was not covered by tests

POSTGRES_TEST_DB = os.environ.get("IBIS_TEST_POSTGRES_DATABASE", "ibis_testing")
IBIS_POSTGRES_HOST = os.environ.get("IBIS_TEST_POSTGRES_HOST", "localhost")
Expand Down Expand Up @@ -260,7 +260,7 @@ def test_kwargs_passthrough_in_connect():

def test_port():
# check that we parse and use the port (and then of course fail cuz it's bogus)
with pytest.raises(PsycoPg2OperationalError):
with pytest.raises(PsycoPgOperationalError):

Check warning on line 263 in ibis/backends/postgres/tests/test_client.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/tests/test_client.py#L263

Added line #L263 was not covered by tests
ibis.connect("postgresql://postgres:postgres@localhost:1337/ibis_testing")


Expand Down Expand Up @@ -388,7 +388,7 @@ def test_password_with_bracket():
quoted_pass = quote_plus(password)
url = f"postgres://{IBIS_POSTGRES_USER}:{quoted_pass}@{IBIS_POSTGRES_HOST}:{IBIS_POSTGRES_PORT}/{POSTGRES_TEST_DB}"
with pytest.raises(
PsycoPg2OperationalError,
PsycoPgOperationalError,
match=f'password authentication failed for user "{IBIS_POSTGRES_USER}"',
):
ibis.connect(url)
Expand Down
2 changes: 1 addition & 1 deletion ibis/backends/postgres/tests/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import ibis.expr.types as ir
from ibis import literal as L

pytest.importorskip("psycopg2")
pytest.importorskip("psycopg")

Check warning on line 19 in ibis/backends/postgres/tests/test_functions.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/tests/test_functions.py#L19

Added line #L19 was not covered by tests


@pytest.mark.parametrize(
Expand Down
2 changes: 1 addition & 1 deletion ibis/backends/postgres/tests/test_postgis.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest
from numpy import testing

pytest.importorskip("psycopg2")
pytest.importorskip("psycopg")

Check warning on line 10 in ibis/backends/postgres/tests/test_postgis.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/tests/test_postgis.py#L10

Added line #L10 was not covered by tests
gpd = pytest.importorskip("geopandas")
pytest.importorskip("shapely")

Expand Down
2 changes: 1 addition & 1 deletion ibis/backends/postgres/tests/test_udf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from ibis import udf
from ibis.util import guid

pytest.importorskip("psycopg2")
pytest.importorskip("psycopg")

Check warning on line 15 in ibis/backends/postgres/tests/test_udf.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/postgres/tests/test_udf.py#L15

Added line #L15 was not covered by tests


@pytest.fixture(scope="session")
Expand Down
19 changes: 19 additions & 0 deletions ibis/backends/tests/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,25 @@
except ImportError:
TrinoUserError = None

try:
from psycopg.errors import ArraySubscriptError as PsycoPgArraySubscriptError
from psycopg.errors import DivisionByZero as PsycoPgDivisionByZero
from psycopg.errors import IndeterminateDatatype as PsycoPgIndeterminateDatatype
from psycopg.errors import InternalError_ as PsycoPgInternalError
from psycopg.errors import (

Check warning on line 120 in ibis/backends/tests/errors.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/tests/errors.py#L117-L120

Added lines #L117 - L120 were not covered by tests
InvalidTextRepresentation as PsycoPgInvalidTextRepresentation,
)
from psycopg.errors import OperationalError as PsycoPgOperationalError
from psycopg.errors import ProgrammingError as PsycoPgProgrammingError
from psycopg.errors import SyntaxError as PsycoPgSyntaxError
from psycopg.errors import UndefinedObject as PsycoPgUndefinedObject

Check warning on line 126 in ibis/backends/tests/errors.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/tests/errors.py#L123-L126

Added lines #L123 - L126 were not covered by tests
except ImportError:
PsycoPgSyntaxError = PsycoPgIndeterminateDatatype = (
PsycoPgInvalidTextRepresentation
) = PsycoPgDivisionByZero = PsycoPgInternalError = PsycoPgProgrammingError = (
PsycoPgOperationalError
) = PsycoPgUndefinedObject = PsycoPgArraySubscriptError = None

try:
from psycopg2.errors import ArraySubscriptError as PsycoPg2ArraySubscriptError
from psycopg2.errors import DivisionByZero as PsycoPg2DivisionByZero
Expand Down
27 changes: 19 additions & 8 deletions ibis/backends/tests/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
GoogleBadRequest,
MySQLOperationalError,
PolarsComputeError,
PsycoPg2ArraySubscriptError,
PsycoPg2IndeterminateDatatype,
PsycoPg2InternalError,
PsycoPg2ProgrammingError,
PsycoPg2SyntaxError,
PsycoPgIndeterminateDatatype,
PsycoPgInternalError,
PsycoPgInvalidTextRepresentation,
PsycoPgSyntaxError,
Py4JJavaError,
PyAthenaDatabaseError,
PyAthenaOperationalError,
Expand Down Expand Up @@ -1094,7 +1096,7 @@ def test_array_intersect(con, data):


@builtin_array
@pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError)
@pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError)
@pytest.mark.notimpl(["risingwave"], raises=PsycoPg2InternalError)
@pytest.mark.notimpl(
["trino"], reason="inserting maps into structs doesn't work", raises=TrinoUserError
Expand All @@ -1114,7 +1116,7 @@ def test_unnest_struct(con):


@builtin_array
@pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError)
@pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError)
@pytest.mark.notimpl(["risingwave"], raises=PsycoPg2InternalError)
@pytest.mark.notimpl(
["trino"], reason="inserting maps into structs doesn't work", raises=TrinoUserError
Expand Down Expand Up @@ -1205,7 +1207,7 @@ def test_zip_null(con, fn):


@builtin_array
@pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError)
@pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError)
@pytest.mark.notimpl(["risingwave"], raises=PsycoPg2ProgrammingError)
@pytest.mark.notimpl(["datafusion"], raises=Exception, reason="not yet supported")
@pytest.mark.notimpl(
Expand Down Expand Up @@ -1276,8 +1278,17 @@ def flatten_data():
["bigquery"], reason="BigQuery doesn't support arrays of arrays", raises=TypeError
)
@pytest.mark.notyet(
["postgres", "risingwave"],
["postgres"],
reason="Postgres doesn't truly support arrays of arrays",
raises=(
com.OperationNotDefinedError,
PsycoPgIndeterminateDatatype,
PsycoPgInternalError,
),
)
@pytest.mark.notyet(
["risingwave"],
reason="Risingwave doesn't truly support arrays of arrays",
raises=(
com.OperationNotDefinedError,
PsycoPg2IndeterminateDatatype,
Expand Down Expand Up @@ -1769,7 +1780,7 @@ def test_table_unnest_column_expr(backend):
)
@pytest.mark.notimpl(["trino"], raises=TrinoUserError)
@pytest.mark.notimpl(["athena"], raises=PyAthenaOperationalError)
@pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError)
@pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError)
@pytest.mark.notimpl(["risingwave"], raises=PsycoPg2ProgrammingError)
@pytest.mark.notyet(
["risingwave"], raises=PsycoPg2InternalError, reason="not supported in risingwave"
Expand Down Expand Up @@ -1887,7 +1898,7 @@ def test_array_agg_bool(con, data, agg, baseline_func):

@pytest.mark.notyet(
["postgres"],
raises=PsycoPg2ArraySubscriptError,
raises=PsycoPgInvalidTextRepresentation,
reason="all dimensions must match in size",
)
@pytest.mark.notimpl(["risingwave", "flink"], raises=com.OperationNotDefinedError)
Expand Down
4 changes: 2 additions & 2 deletions ibis/backends/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
ImpalaHiveServer2Error,
OracleDatabaseError,
PsycoPg2InternalError,
PsycoPg2UndefinedObject,
PsycoPgUndefinedObject,
Py4JJavaError,
PyAthenaDatabaseError,
PyODBCProgrammingError,
Expand Down Expand Up @@ -725,7 +725,7 @@ def test_list_database_contents(con):
@pytest.mark.notyet(["databricks"], raises=DatabricksServerOperationError)
@pytest.mark.notyet(["bigquery"], raises=com.UnsupportedBackendType)
@pytest.mark.notyet(
["postgres"], raises=PsycoPg2UndefinedObject, reason="no unsigned int types"
["postgres"], raises=PsycoPgUndefinedObject, reason="no unsigned int types"
)
@pytest.mark.notyet(
["oracle"], raises=OracleDatabaseError, reason="no unsigned int types"
Expand Down
4 changes: 2 additions & 2 deletions ibis/backends/tests/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
OracleDatabaseError,
PolarsInvalidOperationError,
PsycoPg2InternalError,
PsycoPg2SyntaxError,
PsycoPgSyntaxError,
Py4JJavaError,
PyAthenaDatabaseError,
PyAthenaOperationalError,
Expand Down Expand Up @@ -1739,7 +1739,7 @@ def hash_256(col):
pytest.mark.notimpl(["flink"], raises=Py4JJavaError),
pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError),
pytest.mark.notimpl(["oracle"], raises=OracleDatabaseError),
pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError),
pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError),
pytest.mark.notimpl(["risingwave"], raises=PsycoPg2InternalError),
pytest.mark.notimpl(["snowflake"], raises=AssertionError),
pytest.mark.never(
Expand Down
4 changes: 2 additions & 2 deletions ibis/backends/tests/test_numeric.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
ImpalaHiveServer2Error,
MySQLOperationalError,
OracleDatabaseError,
PsycoPg2DivisionByZero,
PsycoPg2InternalError,
PsycoPgDivisionByZero,
Py4JError,
Py4JJavaError,
PyAthenaOperationalError,
Expand Down Expand Up @@ -1323,7 +1323,7 @@ def test_floating_mod(backend, alltypes, df):
)
@pytest.mark.notyet(["mssql"], raises=PyODBCDataError)
@pytest.mark.notyet(["snowflake"], raises=SnowflakeProgrammingError)
@pytest.mark.notyet(["postgres"], raises=PsycoPg2DivisionByZero)
@pytest.mark.notyet(["postgres"], raises=PsycoPgDivisionByZero)
@pytest.mark.notimpl(["exasol"], raises=ExaQueryError)
@pytest.mark.xfail_version(duckdb=["duckdb<1.1"])
def test_divide_by_zero(backend, alltypes, df, column, denominator):
Expand Down
Loading

0 comments on commit 7f1292b

Please sign in to comment.