Skip to content

Commit e7a3e2c

Browse files
committed
Add relaxed variants of the built-in SQLAlchemy PostgreSQL dialects
The corresponding dialect identifiers are: - `postgresql+psycopg_relaxed` - `postgresql+asyncpg_relaxed`
1 parent 8e2c04b commit e7a3e2c

File tree

6 files changed

+147
-0
lines changed

6 files changed

+147
-0
lines changed

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ changelog = "https://github.com/daq-tools/sqlalchemy-postgresql-relaxed/blob/mai
8585
documentation = "https://github.com/daq-tools/sqlalchemy-postgresql-relaxed"
8686
homepage = "https://github.com/daq-tools/sqlalchemy-postgresql-relaxed"
8787
repository = "https://github.com/daq-tools/sqlalchemy-postgresql-relaxed"
88+
[project.entry-points."sqlalchemy.dialects"]
89+
"postgresql.asyncpg_relaxed" = "sqlalchemy_postgresql_relaxed.asyncpg:dialect"
90+
"postgresql.psycopg_relaxed" = "sqlalchemy_postgresql_relaxed.psycopg:dialect"
91+
"postgresql.psycopg_relaxed_async" = "sqlalchemy_postgresql_relaxed.psycopg:dialect_async"
8892

8993
[tool.setuptools.packages]
9094
find = {}

sqlalchemy_postgresql_relaxed/__init__.py

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from sqlalchemy.dialects.postgresql.asyncpg import PGDialect_asyncpg
2+
3+
from sqlalchemy_postgresql_relaxed.base import PGDialect_relaxed
4+
5+
6+
class PGDialect_asyncpg_relaxed(PGDialect_relaxed, PGDialect_asyncpg):
7+
"""
8+
Don't strictly expect JSON and JSONB codecs.
9+
10+
asyncpg.exceptions._base.InterfaceError: cannot use custom codec on non-scalar type pg_catalog.json
11+
"""
12+
13+
driver = "asyncpg_relaxed"
14+
supports_statement_cache = True
15+
16+
async def setup_asyncpg_json_codec(self, conn):
17+
pass
18+
19+
async def setup_asyncpg_jsonb_codec(self, conn):
20+
pass
21+
22+
23+
dialect = PGDialect_asyncpg_relaxed

sqlalchemy_postgresql_relaxed/base.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import re
2+
3+
from sqlalchemy.dialects.postgresql.base import PGDialect
4+
from sqlalchemy.engine.default import DefaultDialect
5+
6+
7+
class PGDialect_relaxed(PGDialect):
8+
def _get_server_version_info(self, connection):
9+
"""
10+
Accept non-conforming server version responses by adjusting signature matching pattern.
11+
12+
AssertionError: Could not determine version from string
13+
'CrateDB 5.2.2 (built a33862e/2023-02-09T10:26:09Z, Linux 5.10.124-linuxkit amd64, OpenJDK 64-Bit Server VM 19.0.2+7)'
14+
""" # noqa: E501
15+
v = connection.exec_driver_sql("select pg_catalog.version()").scalar()
16+
m = re.match(
17+
r".*(?:PostgreSQL|EnterpriseDB|\w+?) " r"(\d+)\.?(\d+)?(?:\.(\d+))?(?:\.\d+)?(?:devel|beta)?",
18+
v,
19+
)
20+
if not m:
21+
raise AssertionError("Could not determine version from string '%s'" % v)
22+
return tuple([int(x) for x in m.group(1, 2, 3) if x is not None])
23+
24+
def initialize(self, connection):
25+
"""
26+
Don't issue `SHOW STANDARD_CONFORMING_STRINGS` inquiry.
27+
28+
sqlalchemy.exc.InternalError: (psycopg.errors.InternalError_)
29+
Unknown session setting name 'standard_conforming_strings'.
30+
"""
31+
DefaultDialect.initialize(self, connection)
32+
33+
# https://www.postgresql.org/docs/9.3/static/release-9-2.html#AEN116689
34+
self.supports_smallserial = self.server_version_info >= (9, 2)
35+
36+
# PATCH. Let's assume "on"?
37+
"""
38+
std_string = connection.exec_driver_sql(
39+
"show standard_conforming_strings"
40+
).scalar()
41+
"""
42+
std_string = "on"
43+
44+
self._backslash_escapes = std_string == "off"
45+
46+
self._supports_drop_index_concurrently = self.server_version_info >= (
47+
9,
48+
2,
49+
)
50+
self.supports_identity_columns = self.server_version_info >= (10,)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from sqlalchemy.dialects.postgresql.psycopg import PGDialect_psycopg, PGDialectAsync_psycopg
2+
3+
from sqlalchemy_postgresql_relaxed.base import PGDialect_relaxed
4+
5+
6+
class PGDialect_psycopg_relaxed(PGDialect_relaxed, PGDialect_psycopg):
7+
driver = "psycopg_relaxed"
8+
9+
@classmethod
10+
def get_async_dialect_cls(cls, url):
11+
return PGDialectAsync_psycopg_relaxed
12+
13+
14+
class PGDialectAsync_psycopg_relaxed(PGDialect_psycopg_relaxed, PGDialectAsync_psycopg):
15+
is_async = True
16+
supports_statement_cache = True
17+
driver = "psycopg_async_relaxed"
18+
19+
20+
dialect = PGDialect_psycopg_relaxed
21+
dialect_async = PGDialectAsync_psycopg_relaxed

tests/test_cratedb.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import pytest
2+
import sqlalchemy as sa
3+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
4+
5+
6+
def test_psycopg_sync():
7+
engine = sa.create_engine(
8+
url="postgresql+psycopg_relaxed://crate@localhost/acme",
9+
isolation_level="AUTOCOMMIT",
10+
use_native_hstore=False,
11+
echo=True,
12+
)
13+
with engine.connect() as con:
14+
result = con.exec_driver_sql("SELECT 'Kill all humans.';")
15+
assert result.scalar_one() == "Kill all humans."
16+
con.close()
17+
18+
19+
@pytest.mark.asyncio
20+
async def test_psycopg_async():
21+
engine = create_async_engine(
22+
url="postgresql+psycopg_relaxed://crate@localhost/acme",
23+
isolation_level="AUTOCOMMIT",
24+
use_native_hstore=False,
25+
echo=True,
26+
)
27+
async_session = async_sessionmaker(engine)
28+
session: AsyncSession
29+
async with async_session() as session:
30+
result = await session.execute(sa.text("SELECT 'Kill all humans.';"))
31+
assert result.scalar_one() == "Kill all humans."
32+
await session.close()
33+
await engine.dispose()
34+
35+
36+
@pytest.mark.asyncio
37+
async def test_asyncpg():
38+
engine = create_async_engine(
39+
url="postgresql+asyncpg_relaxed://crate@localhost/acme",
40+
isolation_level="AUTOCOMMIT",
41+
echo=True,
42+
)
43+
async_session = async_sessionmaker(engine)
44+
session: AsyncSession
45+
async with async_session() as session:
46+
result = await session.execute(sa.text("SELECT 'Kill all humans.';"))
47+
assert result.scalar_one() == "Kill all humans."
48+
await session.close()
49+
await engine.dispose()

0 commit comments

Comments
 (0)