Skip to content

Commit 0345ad5

Browse files
committed
Use psycopg rather than psycopg2 for Postgres
1 parent b6e9ef1 commit 0345ad5

19 files changed

+101
-57
lines changed

.github/renovate.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
},
7070
{
7171
"addLabels": ["postgres"],
72-
"matchPackageNames": ["/psycopg2/", "/postgres/"]
72+
"matchPackageNames": ["/psycopg/", "/postgres/"]
7373
},
7474
{
7575
"addLabels": ["druid"],
@@ -89,7 +89,7 @@
8989
},
9090
{
9191
"addLabels": ["risingwave"],
92-
"matchPackageNames": ["/risingwave/"]
92+
"matchPackageNames": ["/psycopg2/", "/risingwave/"]
9393
},
9494
{
9595
"addLabels": ["snowflake"],

conda/environment-arm64-flink.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ dependencies:
2626
- pins >=0.8.2
2727
- uv>=0.4.29
2828
- polars >=1,<2
29-
- psycopg2 >=2.8.4
29+
- psycopg >= 3.2.0
3030
- pyarrow =11.0.0
3131
- pyarrow-tests
3232
- pyarrow-hotfix >=0.4

conda/environment-arm64.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ dependencies:
2626
- pins >=0.8.2
2727
- uv>=0.4.29
2828
- polars >=1,<2
29-
- psycopg2 >=2.8.4
29+
- psycopg >= 3.2.0
3030
- pyarrow >=10.0.1
3131
- pyarrow-tests
3232
- pyarrow-hotfix >=0.4

conda/environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ dependencies:
2727
- pip
2828
- uv>=0.4.29
2929
- polars >=1,<2
30-
- psycopg2 >=2.8.4
30+
- psycopg >= 3.2.0
3131
- pyarrow >=10.0.1
3232
- pyarrow-hotfix >=0.4
3333
- pydata-google-auth

ibis/backends/postgres/__init__.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
import pandas as pd
3333
import polars as pl
34-
import psycopg2
34+
import psycopg
3535
import pyarrow as pa
3636

3737

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

9292
def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
93-
from psycopg2.extras import execute_batch
94-
9593
schema = op.schema
9694
if null_columns := [col for col, dtype in schema.items() if dtype.is_null()]:
9795
raise exc.IbisTypeError(
@@ -129,7 +127,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
129127

130128
with self.begin() as cur:
131129
cur.execute(create_stmt_sql)
132-
execute_batch(cur, sql, data, 128)
130+
cur.executemany(sql, data)
133131

134132
@contextlib.contextmanager
135133
def begin(self):
@@ -145,14 +143,16 @@ def begin(self):
145143
finally:
146144
cursor.close()
147145

148-
def _fetch_from_cursor(self, cursor, schema: sch.Schema) -> pd.DataFrame:
146+
def _fetch_from_cursor(
147+
self, cursor: psycopg.Cursor, schema: sch.Schema
148+
) -> pd.DataFrame:
149149
import pandas as pd
150150

151151
from ibis.backends.postgres.converter import PostgresPandasData
152152

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

167167
@property
168168
def version(self):
169-
version = f"{self.con.server_version:0>6}"
169+
version = f"{self.con.info.server_version:0>6}"
170170
major = int(version[:2])
171171
minor = int(version[2:4])
172172
patch = int(version[4:])
@@ -233,17 +233,17 @@ def do_connect(
233233
year int32
234234
month int32
235235
"""
236-
import psycopg2
237-
import psycopg2.extras
236+
import psycopg
237+
import psycopg.types.json
238238

239-
psycopg2.extras.register_default_json(loads=lambda x: x)
239+
psycopg.types.json.set_json_loads(loads=lambda x: x)
240240

241-
self.con = psycopg2.connect(
241+
self.con = psycopg.connect(
242242
host=host,
243243
port=port,
244244
user=user,
245245
password=password,
246-
database=database,
246+
dbname=database,
247247
options=(f"-csearch_path={schema}" * (schema is not None)) or None,
248248
**kwargs,
249249
)
@@ -252,7 +252,7 @@ def do_connect(
252252

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

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

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

713714
try:
714-
# try to load hstore, uuid and ipaddress extensions
715-
with contextlib.suppress(psycopg2.ProgrammingError):
716-
psycopg2.extras.register_hstore(cursor)
717-
with contextlib.suppress(psycopg2.ProgrammingError):
718-
psycopg2.extras.register_uuid(conn_or_curs=cursor)
719-
with contextlib.suppress(psycopg2.ProgrammingError):
720-
psycopg2.extras.register_ipaddress(cursor)
715+
# try to load hstore
716+
with contextlib.suppress(TypeError):
717+
type_info = psycopg.types.TypeInfo.fetch(con, "hstore")
718+
with contextlib.suppress(psycopg.ProgrammingError, TypeError):
719+
psycopg.types.hstore.register_hstore(type_info, cursor)
721720
except Exception:
722721
cursor.close()
723722
raise

ibis/backends/postgres/tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class TestConf(ServiceBackendTest):
4848
supports_structs = False
4949
rounding_method = "half_to_even"
5050
service_name = "postgres"
51-
deps = ("psycopg2",)
51+
deps = ("psycopg",)
5252

5353
driver_supports_multiple_statements = True
5454

ibis/backends/postgres/tests/test_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@
3030
import ibis.common.exceptions as com
3131
import ibis.expr.datatypes as dt
3232
import ibis.expr.types as ir
33-
from ibis.backends.tests.errors import PsycoPg2OperationalError
33+
from ibis.backends.tests.errors import PsycoPgOperationalError
3434
from ibis.util import gen_name
3535

36-
pytest.importorskip("psycopg2")
36+
pytest.importorskip("psycopg")
3737

3838
POSTGRES_TEST_DB = os.environ.get("IBIS_TEST_POSTGRES_DATABASE", "ibis_testing")
3939
IBIS_POSTGRES_HOST = os.environ.get("IBIS_TEST_POSTGRES_HOST", "localhost")
@@ -260,7 +260,7 @@ def test_kwargs_passthrough_in_connect():
260260

261261
def test_port():
262262
# check that we parse and use the port (and then of course fail cuz it's bogus)
263-
with pytest.raises(PsycoPg2OperationalError):
263+
with pytest.raises(PsycoPgOperationalError):
264264
ibis.connect("postgresql://postgres:postgres@localhost:1337/ibis_testing")
265265

266266

@@ -388,7 +388,7 @@ def test_password_with_bracket():
388388
quoted_pass = quote_plus(password)
389389
url = f"postgres://{IBIS_POSTGRES_USER}:{quoted_pass}@{IBIS_POSTGRES_HOST}:{IBIS_POSTGRES_PORT}/{POSTGRES_TEST_DB}"
390390
with pytest.raises(
391-
PsycoPg2OperationalError,
391+
PsycoPgOperationalError,
392392
match=f'password authentication failed for user "{IBIS_POSTGRES_USER}"',
393393
):
394394
ibis.connect(url)

ibis/backends/postgres/tests/test_functions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import ibis.expr.types as ir
1717
from ibis import literal as L
1818

19-
pytest.importorskip("psycopg2")
19+
pytest.importorskip("psycopg")
2020

2121

2222
@pytest.mark.parametrize(
@@ -1212,7 +1212,7 @@ def test_string_to_binary_round_trip(con):
12121212
)
12131213
with con.begin() as c:
12141214
c.execute(sql_string)
1215-
rows = [row[0] for (row,) in c.fetchall()]
1215+
rows = [row[0] for row in c.fetchall()]
12161216
expected = pd.Series(rows, name=name)
12171217
tm.assert_series_equal(result, expected)
12181218

ibis/backends/postgres/tests/test_postgis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pytest
88
from numpy import testing
99

10-
pytest.importorskip("psycopg2")
10+
pytest.importorskip("psycopg")
1111
gpd = pytest.importorskip("geopandas")
1212
pytest.importorskip("shapely")
1313

ibis/backends/postgres/tests/test_udf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from ibis import udf
1313
from ibis.util import guid
1414

15-
pytest.importorskip("psycopg2")
15+
pytest.importorskip("psycopg")
1616

1717

1818
@pytest.fixture(scope="session")

ibis/backends/tests/errors.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,25 @@
112112
except ImportError:
113113
TrinoUserError = None
114114

115+
try:
116+
from psycopg.errors import ArraySubscriptError as PsycoPgArraySubscriptError
117+
from psycopg.errors import DivisionByZero as PsycoPgDivisionByZero
118+
from psycopg.errors import IndeterminateDatatype as PsycoPgIndeterminateDatatype
119+
from psycopg.errors import InternalError_ as PsycoPgInternalError
120+
from psycopg.errors import (
121+
InvalidTextRepresentation as PsycoPgInvalidTextRepresentation,
122+
)
123+
from psycopg.errors import OperationalError as PsycoPgOperationalError
124+
from psycopg.errors import ProgrammingError as PsycoPgProgrammingError
125+
from psycopg.errors import SyntaxError as PsycoPgSyntaxError
126+
from psycopg.errors import UndefinedObject as PsycoPgUndefinedObject
127+
except ImportError:
128+
PsycoPgSyntaxError = PsycoPgIndeterminateDatatype = (
129+
PsycoPgInvalidTextRepresentation
130+
) = PsycoPgDivisionByZero = PsycoPgInternalError = PsycoPgProgrammingError = (
131+
PsycoPgOperationalError
132+
) = PsycoPgUndefinedObject = PsycoPgArraySubscriptError = None
133+
115134
try:
116135
from psycopg2.errors import ArraySubscriptError as PsycoPg2ArraySubscriptError
117136
from psycopg2.errors import DivisionByZero as PsycoPg2DivisionByZero

ibis/backends/tests/test_array.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
GoogleBadRequest,
2323
MySQLOperationalError,
2424
PolarsComputeError,
25-
PsycoPg2ArraySubscriptError,
2625
PsycoPg2IndeterminateDatatype,
2726
PsycoPg2InternalError,
2827
PsycoPg2ProgrammingError,
29-
PsycoPg2SyntaxError,
28+
PsycoPgIndeterminateDatatype,
29+
PsycoPgInternalError,
30+
PsycoPgInvalidTextRepresentation,
31+
PsycoPgSyntaxError,
3032
Py4JJavaError,
3133
PyAthenaDatabaseError,
3234
PyAthenaOperationalError,
@@ -1094,7 +1096,7 @@ def test_array_intersect(con, data):
10941096

10951097

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

11151117

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

12061208

12071209
@builtin_array
1208-
@pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError)
1210+
@pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError)
12091211
@pytest.mark.notimpl(["risingwave"], raises=PsycoPg2ProgrammingError)
12101212
@pytest.mark.notimpl(["datafusion"], raises=Exception, reason="not yet supported")
12111213
@pytest.mark.notimpl(
@@ -1276,8 +1278,17 @@ def flatten_data():
12761278
["bigquery"], reason="BigQuery doesn't support arrays of arrays", raises=TypeError
12771279
)
12781280
@pytest.mark.notyet(
1279-
["postgres", "risingwave"],
1281+
["postgres"],
12801282
reason="Postgres doesn't truly support arrays of arrays",
1283+
raises=(
1284+
com.OperationNotDefinedError,
1285+
PsycoPgIndeterminateDatatype,
1286+
PsycoPgInternalError,
1287+
),
1288+
)
1289+
@pytest.mark.notyet(
1290+
["risingwave"],
1291+
reason="Risingwave doesn't truly support arrays of arrays",
12811292
raises=(
12821293
com.OperationNotDefinedError,
12831294
PsycoPg2IndeterminateDatatype,
@@ -1769,7 +1780,7 @@ def test_table_unnest_column_expr(backend):
17691780
)
17701781
@pytest.mark.notimpl(["trino"], raises=TrinoUserError)
17711782
@pytest.mark.notimpl(["athena"], raises=PyAthenaOperationalError)
1772-
@pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError)
1783+
@pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError)
17731784
@pytest.mark.notimpl(["risingwave"], raises=PsycoPg2ProgrammingError)
17741785
@pytest.mark.notyet(
17751786
["risingwave"], raises=PsycoPg2InternalError, reason="not supported in risingwave"
@@ -1887,7 +1898,7 @@ def test_array_agg_bool(con, data, agg, baseline_func):
18871898

18881899
@pytest.mark.notyet(
18891900
["postgres"],
1890-
raises=PsycoPg2ArraySubscriptError,
1901+
raises=PsycoPgInvalidTextRepresentation,
18911902
reason="all dimensions must match in size",
18921903
)
18931904
@pytest.mark.notimpl(["risingwave", "flink"], raises=com.OperationNotDefinedError)

ibis/backends/tests/test_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
ImpalaHiveServer2Error,
3333
OracleDatabaseError,
3434
PsycoPg2InternalError,
35-
PsycoPg2UndefinedObject,
35+
PsycoPgUndefinedObject,
3636
Py4JJavaError,
3737
PyAthenaDatabaseError,
3838
PyODBCProgrammingError,
@@ -725,7 +725,7 @@ def test_list_database_contents(con):
725725
@pytest.mark.notyet(["databricks"], raises=DatabricksServerOperationError)
726726
@pytest.mark.notyet(["bigquery"], raises=com.UnsupportedBackendType)
727727
@pytest.mark.notyet(
728-
["postgres"], raises=PsycoPg2UndefinedObject, reason="no unsigned int types"
728+
["postgres"], raises=PsycoPgUndefinedObject, reason="no unsigned int types"
729729
)
730730
@pytest.mark.notyet(
731731
["oracle"], raises=OracleDatabaseError, reason="no unsigned int types"

ibis/backends/tests/test_generic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
OracleDatabaseError,
2626
PolarsInvalidOperationError,
2727
PsycoPg2InternalError,
28-
PsycoPg2SyntaxError,
28+
PsycoPgSyntaxError,
2929
Py4JJavaError,
3030
PyAthenaDatabaseError,
3131
PyAthenaOperationalError,
@@ -1739,7 +1739,7 @@ def hash_256(col):
17391739
pytest.mark.notimpl(["flink"], raises=Py4JJavaError),
17401740
pytest.mark.notimpl(["druid"], raises=PyDruidProgrammingError),
17411741
pytest.mark.notimpl(["oracle"], raises=OracleDatabaseError),
1742-
pytest.mark.notimpl(["postgres"], raises=PsycoPg2SyntaxError),
1742+
pytest.mark.notimpl(["postgres"], raises=PsycoPgSyntaxError),
17431743
pytest.mark.notimpl(["risingwave"], raises=PsycoPg2InternalError),
17441744
pytest.mark.notimpl(["snowflake"], raises=AssertionError),
17451745
pytest.mark.never(

ibis/backends/tests/test_numeric.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
ImpalaHiveServer2Error,
2323
MySQLOperationalError,
2424
OracleDatabaseError,
25-
PsycoPg2DivisionByZero,
2625
PsycoPg2InternalError,
26+
PsycoPgDivisionByZero,
2727
Py4JError,
2828
Py4JJavaError,
2929
PyAthenaOperationalError,
@@ -1323,7 +1323,7 @@ def test_floating_mod(backend, alltypes, df):
13231323
)
13241324
@pytest.mark.notyet(["mssql"], raises=PyODBCDataError)
13251325
@pytest.mark.notyet(["snowflake"], raises=SnowflakeProgrammingError)
1326-
@pytest.mark.notyet(["postgres"], raises=PsycoPg2DivisionByZero)
1326+
@pytest.mark.notyet(["postgres"], raises=PsycoPgDivisionByZero)
13271327
@pytest.mark.notimpl(["exasol"], raises=ExaQueryError)
13281328
@pytest.mark.xfail_version(duckdb=["duckdb<1.1"])
13291329
def test_divide_by_zero(backend, alltypes, df, column, denominator):

0 commit comments

Comments
 (0)