Skip to content

Commit 9a5ce68

Browse files
author
Eugene Shershen
committed
codeql-analysis.yml
1 parent a1c67a2 commit 9a5ce68

4 files changed

Lines changed: 264 additions & 1 deletion

File tree

.github/workflows/codeql-analysis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,4 @@ jobs:
6868
- name: Perform CodeQL Analysis
6969
uses: github/codeql-action/analyze@v3
7070
with:
71-
category: "/language:${{matrix.language}}"
71+
category: "/language:${{matrix.language}}"

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This package provides a SQLAlchemy dialect that allows you to use psqlpy as the
1010

1111
- **High Performance**: Built on psqlpy's Rust-based PostgreSQL driver
1212
- **SQLAlchemy 2.0+ Compatible**: Full support for modern SQLAlchemy features
13+
- **SQLModel Compatible**: Works with SQLModel for Pydantic integration
1314
- **DBAPI 2.0 Compliant**: Standard Python database interface
1415
- **Connection Pooling**: Leverages psqlpy's built-in connection pooling
1516
- **Transaction Support**: Full transaction and savepoint support
@@ -97,6 +98,40 @@ with engine.connect() as conn:
9798
print(row)
9899
```
99100

101+
### SQLModel Usage
102+
103+
```python
104+
from typing import Optional
105+
from sqlmodel import Field, Session, SQLModel, create_engine, select
106+
107+
# Define a SQLModel model
108+
class Hero(SQLModel, table=True):
109+
id: Optional[int] = Field(default=None, primary_key=True)
110+
name: str
111+
secret_name: str
112+
age: Optional[int] = None
113+
114+
# Create engine with psqlpy dialect
115+
engine = create_engine("postgresql+psqlpy://user:password@localhost/testdb")
116+
117+
# Create tables
118+
SQLModel.metadata.create_all(engine)
119+
120+
# Insert data
121+
with Session(engine) as session:
122+
hero = Hero(name="Deadpond", secret_name="Dive Wilson", age=30)
123+
session.add(hero)
124+
session.commit()
125+
session.refresh(hero)
126+
print(f"Created hero: {hero.name} with id {hero.id}")
127+
128+
# Query data
129+
with Session(engine) as session:
130+
statement = select(Hero).where(Hero.name == "Deadpond")
131+
hero = session.exec(statement).first()
132+
print(f"Found hero: {hero.name}, secret identity: {hero.secret_name}")
133+
```
134+
100135
### Async Usage
101136

102137
While this dialect provides a synchronous interface, psqlpy itself is async-native. For async SQLAlchemy usage, you would typically use SQLAlchemy's async features:
@@ -207,6 +242,7 @@ This project is licensed under the MIT License - see the LICENSE file for detail
207242

208243
- [psqlpy](https://github.com/qaspen-python/psqlpy) - The underlying PostgreSQL driver
209244
- [SQLAlchemy](https://www.sqlalchemy.org/) - The Python SQL toolkit and ORM
245+
- [SQLModel](https://sqlmodel.tiangolo.com/) - SQLAlchemy-based ORM with Pydantic validation
210246

211247
## Changelog
212248

@@ -215,6 +251,7 @@ This project is licensed under the MIT License - see the LICENSE file for detail
215251
- Initial release
216252
- Basic SQLAlchemy dialect implementation
217253
- DBAPI 2.0 compatible interface
254+
- SQLModel compatibility
218255
- Connection string parsing
219256
- Basic SQL compilation support
220257
- Transaction support

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dev = [
4040
"isort>=5.0.0",
4141
"mypy>=1.0.0",
4242
"flake8>=6.0.0",
43+
"sqlmodel>=0.0.8",
4344
]
4445

4546
[project.urls]
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Tests for SQLModel compatibility with psqlpy-sqlalchemy dialect
4+
"""
5+
6+
import unittest
7+
from typing import Optional
8+
9+
import pytest
10+
from sqlalchemy import create_engine
11+
from sqlmodel import Field, Session, SQLModel, select
12+
13+
14+
class Hero(SQLModel, table=True):
15+
"""Test model for SQLModel compatibility tests"""
16+
17+
id: Optional[int] = Field(default=None, primary_key=True)
18+
name: str
19+
secret_name: str
20+
age: Optional[int] = None
21+
22+
23+
class TestSQLModelCompatibility(unittest.TestCase):
24+
"""Test cases for SQLModel compatibility with psqlpy-sqlalchemy dialect"""
25+
26+
def setUp(self):
27+
"""Set up test fixtures before each test method."""
28+
# Create an in-memory SQLite database for testing
29+
# We use SQLite for testing as it doesn't require a running PostgreSQL server
30+
# The focus is on dialect compatibility with SQLModel, not PostgreSQL features
31+
self.engine = create_engine("sqlite:///:memory:")
32+
33+
# Create all tables
34+
SQLModel.metadata.create_all(self.engine)
35+
36+
# Add some test data
37+
with Session(self.engine) as session:
38+
session.add(
39+
Hero(name="Deadpond", secret_name="Dive Wilson", age=30)
40+
)
41+
session.add(
42+
Hero(name="Spider-Boy", secret_name="Pedro Parqueador", age=16)
43+
)
44+
session.add(
45+
Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
46+
)
47+
session.commit()
48+
49+
def tearDown(self):
50+
"""Clean up after each test method."""
51+
SQLModel.metadata.drop_all(self.engine)
52+
53+
def test_sqlmodel_create_and_read(self):
54+
"""Test creating and reading SQLModel objects"""
55+
# Create a new hero
56+
with Session(self.engine) as session:
57+
hero = Hero(
58+
name="Captain America", secret_name="Steve Rogers", age=100
59+
)
60+
session.add(hero)
61+
session.commit()
62+
session.refresh(hero)
63+
64+
# Verify the hero was created with an ID
65+
self.assertIsNotNone(hero.id)
66+
67+
# Read the hero back
68+
statement = select(Hero).where(Hero.name == "Captain America")
69+
result = session.exec(statement).first()
70+
71+
# Verify the hero was read correctly
72+
self.assertIsNotNone(result)
73+
self.assertEqual(result.name, "Captain America")
74+
self.assertEqual(result.secret_name, "Steve Rogers")
75+
self.assertEqual(result.age, 100)
76+
77+
def test_sqlmodel_update(self):
78+
"""Test updating SQLModel objects"""
79+
with Session(self.engine) as session:
80+
# Find a hero to update
81+
statement = select(Hero).where(Hero.name == "Spider-Boy")
82+
hero = session.exec(statement).first()
83+
self.assertIsNotNone(hero)
84+
85+
# Update the hero
86+
hero.age = 17 # Birthday!
87+
session.add(hero)
88+
session.commit()
89+
90+
# Verify the update
91+
updated_hero = session.exec(statement).first()
92+
self.assertEqual(updated_hero.age, 17)
93+
94+
def test_sqlmodel_delete(self):
95+
"""Test deleting SQLModel objects"""
96+
97+
with Session(self.engine) as session:
98+
# Count heroes before deletion
99+
statement = select(Hero)
100+
heroes_before = session.exec(statement).all()
101+
count_before = len(heroes_before)
102+
103+
# Find and delete a hero
104+
statement = select(Hero).where(Hero.name == "Rusty-Man")
105+
hero = session.exec(statement).first()
106+
self.assertIsNotNone(hero)
107+
108+
session.delete(hero)
109+
session.commit()
110+
111+
# Verify the deletion
112+
statement = select(Hero)
113+
heroes_after = session.exec(statement).all()
114+
count_after = len(heroes_after)
115+
116+
self.assertEqual(count_after, count_before - 1)
117+
118+
statement = select(Hero).where(Hero.name == "Rusty-Man")
119+
deleted_hero = session.exec(statement).first()
120+
self.assertIsNone(deleted_hero)
121+
122+
def test_sqlmodel_relationships(self):
123+
"""Test SQLModel relationship handling"""
124+
125+
class Team(SQLModel, table=True):
126+
id: Optional[int] = Field(default=None, primary_key=True)
127+
name: str
128+
headquarters: str
129+
130+
class HeroWithTeam(SQLModel, table=True):
131+
id: Optional[int] = Field(default=None, primary_key=True)
132+
name: str
133+
secret_name: str
134+
age: Optional[int] = None
135+
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
136+
137+
# Create tables for these models
138+
Team.metadata.create_all(self.engine)
139+
HeroWithTeam.metadata.create_all(self.engine)
140+
141+
# Test data with relationships
142+
with Session(self.engine) as session:
143+
# Create teams
144+
avengers = Team(name="Avengers", headquarters="New York")
145+
justice_league = Team(
146+
name="Justice League", headquarters="Washington"
147+
)
148+
session.add(avengers)
149+
session.add(justice_league)
150+
session.commit()
151+
152+
# Create heroes with team relationships
153+
hero1 = HeroWithTeam(
154+
name="Iron Man",
155+
secret_name="Tony Stark",
156+
age=45,
157+
team_id=avengers.id,
158+
)
159+
hero2 = HeroWithTeam(
160+
name="Batman",
161+
secret_name="Bruce Wayne",
162+
age=40,
163+
team_id=justice_league.id,
164+
)
165+
session.add(hero1)
166+
session.add(hero2)
167+
session.commit()
168+
169+
# Query heroes with their teams
170+
statement = select(HeroWithTeam).where(
171+
HeroWithTeam.team_id == avengers.id
172+
)
173+
avengers_heroes = session.exec(statement).all()
174+
175+
self.assertEqual(len(avengers_heroes), 1)
176+
self.assertEqual(avengers_heroes[0].name, "Iron Man")
177+
178+
# Clean up
179+
HeroWithTeam.metadata.drop_all(self.engine)
180+
Team.metadata.drop_all(self.engine)
181+
182+
183+
class TestPsqlpyDialectWithSQLModel(unittest.TestCase):
184+
"""Test cases for psqlpy dialect with SQLModel"""
185+
186+
@pytest.mark.skip("Requires a running PostgreSQL server")
187+
def test_psqlpy_dialect_with_sqlmodel(self):
188+
"""Test using psqlpy dialect with SQLModel"""
189+
# This test requires a running PostgreSQL server
190+
# It's marked as skipped by default
191+
192+
# Create engine with psqlpy dialect
193+
engine = create_engine(
194+
"postgresql+psqlpy://user:password@localhost/dbname"
195+
)
196+
197+
# Create all tables
198+
SQLModel.metadata.create_all(engine)
199+
200+
# Test basic operations
201+
with Session(engine) as session:
202+
# Create
203+
hero = Hero(
204+
name="Black Widow", secret_name="Natasha Romanoff", age=35
205+
)
206+
session.add(hero)
207+
session.commit()
208+
session.refresh(hero)
209+
210+
# Read
211+
statement = select(Hero).where(Hero.name == "Black Widow")
212+
result = session.exec(statement).first()
213+
214+
# Verify
215+
assert result is not None
216+
assert result.name == "Black Widow"
217+
assert result.secret_name == "Natasha Romanoff"
218+
assert result.age == 35
219+
220+
# Clean up
221+
SQLModel.metadata.drop_all(engine)
222+
223+
224+
if __name__ == "__main__":
225+
unittest.main()

0 commit comments

Comments
 (0)