Skip to content

Commit f4ee3c8

Browse files
author
Erick Friis
authored
infra: add min version testing to pr test flow (#24358)
xfailing some sql tests that do not currently work on sqlalchemy v1 #22207 was very much not sqlalchemy v1 compatible. Moving forward, implementations should be compatible with both to pass CI
1 parent 50cb0a0 commit f4ee3c8

File tree

8 files changed

+95
-19
lines changed

8 files changed

+95
-19
lines changed

.github/scripts/get_min_versions.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import sys
22

3-
import tomllib
3+
if sys.version_info >= (3, 11):
4+
import tomllib
5+
else:
6+
# for python 3.10 and below, which doesnt have stdlib tomllib
7+
import tomli as tomllib
8+
49
from packaging.version import parse as parse_version
510
import re
611

.github/workflows/_test.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,21 @@ jobs:
6565
# grep will exit non-zero if the target message isn't found,
6666
# and `set -e` above will cause the step to fail.
6767
echo "$STATUS" | grep 'nothing to commit, working tree clean'
68+
69+
- name: Get minimum versions
70+
working-directory: ${{ inputs.working-directory }}
71+
id: min-version
72+
run: |
73+
poetry run pip install packaging tomli
74+
min_versions="$(poetry run python $GITHUB_WORKSPACE/.github/scripts/get_min_versions.py pyproject.toml)"
75+
echo "min-versions=$min_versions" >> "$GITHUB_OUTPUT"
76+
echo "min-versions=$min_versions"
77+
78+
- name: Run unit tests with minimum dependency versions
79+
if: ${{ steps.min-version.outputs.min-versions != '' }}
80+
env:
81+
MIN_VERSIONS: ${{ steps.min-version.outputs.min-versions }}
82+
run: |
83+
poetry run pip install --force-reinstall $MIN_VERSIONS --editable .
84+
make tests
85+
working-directory: ${{ inputs.working-directory }}

libs/community/langchain_community/chat_message_histories/sql.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
from sqlalchemy.ext.asyncio import (
3333
AsyncEngine,
3434
AsyncSession,
35-
async_sessionmaker,
3635
create_async_engine,
3736
)
3837
from sqlalchemy.orm import (
@@ -44,6 +43,12 @@
4443
sessionmaker,
4544
)
4645

46+
try:
47+
from sqlalchemy.ext.asyncio import async_sessionmaker
48+
except ImportError:
49+
# dummy for sqlalchemy < 2
50+
async_sessionmaker = type("async_sessionmaker", (type,), {}) # type: ignore
51+
4752
logger = logging.getLogger(__name__)
4853

4954

libs/community/langchain_community/storage/sql.py

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,46 +17,72 @@
1717

1818
from langchain_core.stores import BaseStore
1919
from sqlalchemy import (
20-
Engine,
2120
LargeBinary,
21+
Text,
2222
and_,
2323
create_engine,
2424
delete,
2525
select,
2626
)
27+
from sqlalchemy.engine.base import Engine
2728
from sqlalchemy.ext.asyncio import (
2829
AsyncEngine,
2930
AsyncSession,
30-
async_sessionmaker,
3131
create_async_engine,
3232
)
3333
from sqlalchemy.orm import (
3434
Mapped,
3535
Session,
3636
declarative_base,
37-
mapped_column,
3837
sessionmaker,
3938
)
4039

40+
try:
41+
from sqlalchemy.ext.asyncio import async_sessionmaker
42+
except ImportError:
43+
# dummy for sqlalchemy < 2
44+
async_sessionmaker = type("async_sessionmaker", (type,), {}) # type: ignore
45+
4146
Base = declarative_base()
4247

48+
try:
49+
from sqlalchemy.orm import mapped_column
4350

44-
def items_equal(x: Any, y: Any) -> bool:
45-
return x == y
51+
class LangchainKeyValueStores(Base): # type: ignore[valid-type,misc]
52+
"""Table used to save values."""
53+
54+
# ATTENTION:
55+
# Prior to modifying this table, please determine whether
56+
# we should create migrations for this table to make sure
57+
# users do not experience data loss.
58+
__tablename__ = "langchain_key_value_stores"
59+
60+
namespace: Mapped[str] = mapped_column(
61+
primary_key=True, index=True, nullable=False
62+
)
63+
key: Mapped[str] = mapped_column(primary_key=True, index=True, nullable=False)
64+
value = mapped_column(LargeBinary, index=False, nullable=False)
65+
66+
except ImportError:
67+
# dummy for sqlalchemy < 2
68+
from sqlalchemy import Column
69+
70+
class LangchainKeyValueStores(Base): # type: ignore[valid-type,misc,no-redef]
71+
"""Table used to save values."""
4672

73+
# ATTENTION:
74+
# Prior to modifying this table, please determine whether
75+
# we should create migrations for this table to make sure
76+
# users do not experience data loss.
77+
__tablename__ = "langchain_key_value_stores"
4778

48-
class LangchainKeyValueStores(Base): # type: ignore[valid-type,misc]
49-
"""Table used to save values."""
79+
namespace = Column(Text(), primary_key=True, index=True, nullable=False)
80+
key = Column(Text(), primary_key=True, index=True, nullable=False)
81+
value = Column(LargeBinary, index=False, nullable=False)
5082

51-
# ATTENTION:
52-
# Prior to modifying this table, please determine whether
53-
# we should create migrations for this table to make sure
54-
# users do not experience data loss.
55-
__tablename__ = "langchain_key_value_stores"
5683

57-
namespace: Mapped[str] = mapped_column(primary_key=True, index=True, nullable=False)
58-
key: Mapped[str] = mapped_column(primary_key=True, index=True, nullable=False)
59-
value = mapped_column(LargeBinary, index=False, nullable=False)
84+
def items_equal(x: Any, y: Any) -> bool:
85+
return x == y
6086

6187

6288
# This is a fix of original SQLStore.
@@ -135,7 +161,10 @@ def __init__(
135161
self.namespace = namespace
136162

137163
def create_schema(self) -> None:
138-
Base.metadata.create_all(self.engine)
164+
Base.metadata.create_all(self.engine) # problem in sqlalchemy v1
165+
# sqlalchemy.exc.CompileError: (in table 'langchain_key_value_stores',
166+
# column 'namespace'): Can't generate DDL for NullType(); did you forget
167+
# to specify a type on this Column?
139168

140169
async def acreate_schema(self) -> None:
141170
assert isinstance(self.engine, AsyncEngine)

libs/community/langchain_community/utilities/sql_database.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ def get_table_info(self, table_names: Optional[List[str]] = None) -> str:
338338
continue
339339

340340
# Ignore JSON datatyped columns
341-
for k, v in table.columns.items():
341+
for k, v in table.columns.items(): # AttributeError: items in sqlalchemy v1
342342
if type(v.type) is NullType:
343343
table._columns.remove(v)
344344

libs/community/tests/unit_tests/storage/test_sql.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from typing import AsyncGenerator, Generator, cast
22

33
import pytest
4+
import sqlalchemy as sa
45
from langchain.storage._lc_store import create_kv_docstore, create_lc_store
56
from langchain_core.documents import Document
67
from langchain_core.stores import BaseStore
8+
from packaging import version
79

810
from langchain_community.storage.sql import SQLStore
911

12+
is_sqlalchemy_v1 = version.parse(sa.__version__).major == 1
13+
1014

1115
@pytest.fixture
1216
def sql_store() -> Generator[SQLStore, None, None]:
@@ -22,6 +26,7 @@ async def async_sql_store() -> AsyncGenerator[SQLStore, None]:
2226
yield store
2327

2428

29+
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
2530
def test_create_lc_store(sql_store: SQLStore) -> None:
2631
"""Test that a docstore is created from a base store."""
2732
docstore: BaseStore[str, Document] = cast(
@@ -34,6 +39,7 @@ def test_create_lc_store(sql_store: SQLStore) -> None:
3439
assert fetched_doc.metadata == {"key": "value"}
3540

3641

42+
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
3743
def test_create_kv_store(sql_store: SQLStore) -> None:
3844
"""Test that a docstore is created from a base store."""
3945
docstore = create_kv_docstore(sql_store)
@@ -57,6 +63,7 @@ async def test_async_create_kv_store(async_sql_store: SQLStore) -> None:
5763
assert fetched_doc.metadata == {"key": "value"}
5864

5965

66+
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
6067
def test_sample_sql_docstore(sql_store: SQLStore) -> None:
6168
# Set values for keys
6269
sql_store.mset([("key1", b"value1"), ("key2", b"value2")])

libs/community/tests/unit_tests/test_sql_database.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def db_lazy_reflection(engine: Engine) -> SQLDatabase:
5555
return SQLDatabase(engine, lazy_table_reflection=True)
5656

5757

58+
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
5859
def test_table_info(db: SQLDatabase) -> None:
5960
"""Test that table info is constructed properly."""
6061
output = db.table_info
@@ -85,6 +86,7 @@ def test_table_info(db: SQLDatabase) -> None:
8586
assert sorted(" ".join(output.split())) == sorted(" ".join(expected_output.split()))
8687

8788

89+
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
8890
def test_table_info_lazy_reflection(db_lazy_reflection: SQLDatabase) -> None:
8991
"""Test that table info with lazy reflection"""
9092
assert len(db_lazy_reflection._metadata.sorted_tables) == 0
@@ -111,6 +113,7 @@ def test_table_info_lazy_reflection(db_lazy_reflection: SQLDatabase) -> None:
111113
assert db_lazy_reflection._metadata.sorted_tables[1].name == "user"
112114

113115

116+
@pytest.mark.xfail(is_sqlalchemy_v1, reason="SQLAlchemy 1.x issues")
114117
def test_table_info_w_sample_rows(db: SQLDatabase) -> None:
115118
"""Test that table info is constructed properly."""
116119

libs/community/tests/unit_tests/test_sql_database_schema.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
insert,
1919
schema,
2020
)
21+
import sqlalchemy as sa
22+
23+
from packaging import version
2124

2225
from langchain_community.utilities.sql_database import SQLDatabase
2326

@@ -43,6 +46,9 @@
4346
)
4447

4548

49+
@pytest.mark.xfail(
50+
version.parse(sa.__version__).major == 1, reason="SQLAlchemy 1.x issues"
51+
)
4652
def test_table_info() -> None:
4753
"""Test that table info is constructed properly."""
4854
engine = create_engine("duckdb:///:memory:")
@@ -65,6 +71,9 @@ def test_table_info() -> None:
6571
assert sorted(" ".join(output.split())) == sorted(" ".join(expected_output.split()))
6672

6773

74+
@pytest.mark.xfail(
75+
version.parse(sa.__version__).major == 1, reason="SQLAlchemy 1.x issues"
76+
)
6877
def test_sql_database_run() -> None:
6978
"""Test that commands can be run successfully and returned in correct format."""
7079
engine = create_engine("duckdb:///:memory:")

0 commit comments

Comments
 (0)