Skip to content

Commit 13d2bcd

Browse files
authored
docs: test usage sql files (#268)
Updates SQL Usage examples
1 parent 87b4ad5 commit 13d2bcd

18 files changed

+669
-226
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from pathlib import Path
2+
3+
from sqlspec import SQLFileLoader
4+
5+
__all__ = ("create_loader", "test_loader_loads_queries")
6+
7+
8+
def create_loader(tmp_path: Path) -> tuple[SQLFileLoader, list[str]]:
9+
sql_dir = tmp_path / "sql"
10+
sql_dir.mkdir()
11+
12+
sql_file_1 = sql_dir / "queries" / "users.sql"
13+
sql_file_1.parent.mkdir()
14+
sql_file_1.write_text("""
15+
-- name: get_user_by_id)
16+
SELECT * FROM users WHERE id = :user_id;
17+
-- name: list_active_users
18+
SELECT * FROM users WHERE active = 1;
19+
-- name: create_user
20+
INSERT INTO users (name, email) VALUES (:name, :email);
21+
""")
22+
# start-example
23+
from sqlspec.loader import SQLFileLoader
24+
25+
# Create loader
26+
loader = SQLFileLoader()
27+
28+
# Load SQL files
29+
loader.load_sql(sql_file_1)
30+
31+
# Or load from a directory
32+
loader.load_sql(sql_dir)
33+
34+
# List available queries
35+
queries = loader.list_queries()
36+
print(queries) # ['get_user_by_id', 'list_active_users', 'create_user', ...]
37+
# end-example
38+
return loader, queries
39+
40+
41+
def test_loader_loads_queries(tmp_path: Path) -> None:
42+
43+
loader, queries = create_loader(tmp_path)
44+
# Dummy asserts for doc example
45+
assert hasattr(loader, "load_sql")
46+
assert hasattr(loader, "list_queries")
47+
assert isinstance(queries, list)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from pathlib import Path
2+
3+
__all__ = ("test_integration_with_sqlspec",)
4+
5+
6+
def test_integration_with_sqlspec(tmp_path: Path) -> None:
7+
tmp_sql_dir = tmp_path / "sql"
8+
tmp_sql_dir.mkdir()
9+
sql_file = tmp_sql_dir / "queries.sql"
10+
sql_file.write_text("""
11+
-- name: get_user_by_id
12+
SELECT * FROM users WHERE id = :user_id;
13+
""")
14+
# start-example
15+
from sqlspec import SQLSpec
16+
from sqlspec.loader import SQLFileLoader
17+
18+
# Create loader
19+
loader = SQLFileLoader()
20+
loader.load_sql(tmp_path / "sql/")
21+
22+
# Create SQLSpec with loader
23+
spec = SQLSpec(loader=loader)
24+
25+
# Access loader via SQLSpec
26+
user_query = spec._sql_loader.get_sql("get_user_by_id")
27+
# end-example
28+
# Dummy asserts for doc example
29+
assert user_query is not None
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from pathlib import Path
2+
3+
from docs.examples.usage.usage_sql_files_1 import create_loader
4+
from sqlspec import SQLSpec
5+
from sqlspec.adapters.sqlite import SqliteConfig
6+
7+
__all__ = ("test_type_safe_query_execution",)
8+
9+
10+
def test_type_safe_query_execution(tmp_path: Path) -> None:
11+
loader, _queries = create_loader(tmp_path)
12+
# start-example
13+
14+
from pydantic import BaseModel
15+
16+
class User(BaseModel):
17+
id: int
18+
username: str
19+
email: str
20+
21+
# Load and execute with type safety
22+
query = loader.get_sql("get_user_by_id")
23+
24+
spec = SQLSpec(loader=loader)
25+
config = SqliteConfig(pool_config={"database": ":memory:"})
26+
27+
with spec.provide_session(config) as session:
28+
session.execute("""CREATE TABLE users ( id INTEGER PRIMARY KEY, username TEXT, email TEXT)""")
29+
session.execute(
30+
""" INSERT INTO users (id, username, email) VALUES (1, 'alice', '[email protected]'), (2, 'bob', '[email protected]');"""
31+
)
32+
user: User = session.select_one(query, user_id=1, schema_type=User)
33+
# end-example
34+
# Dummy asserts for doc example
35+
assert user.id == 1
36+
assert user.username == "alice"
37+
assert user.email == "[email protected]"
38+
assert query is not None
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from pathlib import Path
2+
3+
__all__ = ("test_user_management_example",)
4+
5+
6+
def test_user_management_example(tmp_path: Path) -> None:
7+
user_sql_path = tmp_path / "sql"
8+
user_sql_path.mkdir(parents=True, exist_ok=True)
9+
user_sql_file = user_sql_path / "users.sql"
10+
user_sql_file.write_text(
11+
"""-- name: create_user
12+
INSERT INTO users (username, email, password_hash) VALUES (:username, :email, :password_hash) RETURNING id, username, email;
13+
-- name: get_user
14+
SELECT id, username, email FROM users WHERE id = :user_id;
15+
-- name: list_users
16+
SELECT id, username, email FROM users WHERE (:status IS NULL OR active = :status) LIMIT :limit OFFSET :offset;
17+
"""
18+
)
19+
# start-example
20+
# Python code
21+
from sqlspec import SQLSpec
22+
from sqlspec.adapters.sqlite import SqliteConfig
23+
from sqlspec.loader import SQLFileLoader
24+
25+
loader = SQLFileLoader()
26+
loader.load_sql(tmp_path / "sql/users.sql")
27+
28+
spec = SQLSpec()
29+
config = SqliteConfig()
30+
spec.add_config(config)
31+
32+
with spec.provide_session(config) as session:
33+
session.execute(
34+
"""CREATE TABLE users ( id INTEGER PRIMARY KEY, username TEXT, email TEXT, password_hash TEXT, active BOOLEAN DEFAULT 1)"""
35+
)
36+
# Create user
37+
create_query = loader.get_sql("create_user")
38+
result = session.execute(
39+
create_query, username="irma", email="[email protected]", password_hash="hashed_password"
40+
)
41+
user = result.one()
42+
user_id = user["id"]
43+
44+
# Get user
45+
get_query = loader.get_sql("get_user")
46+
user = session.execute(get_query, user_id=user_id).one()
47+
48+
# List users
49+
list_query = loader.get_sql("list_users")
50+
session.execute(list_query, status=True, limit=10, offset=0).data
51+
# end-example
52+
# Dummy asserts for doc example
53+
assert hasattr(loader, "load_sql")
54+
assert hasattr(spec, "add_config")
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from pathlib import Path
2+
3+
__all__ = ("test_analytics_queries_example",)
4+
5+
6+
def test_analytics_queries_example(tmp_path: Path) -> None:
7+
from docs.examples.usage.usage_sql_files_1 import create_loader
8+
from sqlspec import SQLSpec
9+
from sqlspec.adapters.sqlite import SqliteConfig
10+
11+
loader, _queries = create_loader(tmp_path)
12+
sql_analytics_path = tmp_path / "sql"
13+
sql_analytics_path.mkdir(parents=True, exist_ok=True)
14+
sql_analytics_file = sql_analytics_path / "analytics.sql"
15+
sql_analytics_file.write_text(
16+
"""-- name: daily_sales
17+
SELECT order_date, SUM(total_amount) AS total_sales
18+
FROM orders
19+
WHERE order_date BETWEEN :start_date AND :end_date
20+
GROUP BY order_date;
21+
-- name: top_products
22+
SELECT product_id, SUM(quantity) AS total_sold
23+
FROM order_items
24+
WHERE order_date >= :start_date
25+
GROUP BY product_id
26+
ORDER BY total_sold DESC
27+
LIMIT :limit;
28+
"""
29+
)
30+
# start-example
31+
import datetime
32+
33+
# Load analytics queries
34+
loader.load_sql(tmp_path / "sql/analytics.sql")
35+
36+
# Run daily sales report
37+
sales_query = loader.get_sql("daily_sales")
38+
config = SqliteConfig()
39+
spec = SQLSpec()
40+
spec.add_config(config)
41+
with spec.provide_session(config) as session:
42+
session.execute("""CREATE TABLE orders ( order_id INTEGER PRIMARY KEY, order_date DATE, total_amount REAL);""")
43+
session.execute("""
44+
CREATE TABLE order_items ( order_item_id INTEGER PRIMARY KEY, order_id INTEGER, product_id INTEGER, quantity INTEGER, order_date DATE);""")
45+
46+
# Insert sample data
47+
session.execute("""
48+
INSERT INTO orders (order_id, order_date, total_amount) VALUES
49+
(1, '2025-01-05', 150.00),
50+
(2, '2025-01-15', 200.00),
51+
(3, '2025-01-20', 250.00);
52+
""")
53+
session.execute("""
54+
INSERT INTO order_items (order_item_id, order_id, product_id, quantity, order_date) VALUES
55+
(1, 1, 101, 2, '2025-01-05'),
56+
(2, 2, 102, 3, '2025-01-15'),
57+
(3, 3, 101, 1, '2025-01-20');
58+
""")
59+
session.execute(sales_query, start_date=datetime.date(2025, 1, 1), end_date=datetime.date(2025, 2, 1)).data
60+
61+
# Top products
62+
products_query = loader.get_sql("top_products")
63+
session.execute(products_query, start_date=datetime.date(2025, 1, 1), limit=10).data
64+
# end-example
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from pathlib import Path
2+
3+
from pytest_databases.docker.postgres import PostgresService
4+
5+
from sqlspec import SQLFileLoader
6+
from sqlspec.adapters.asyncpg import AsyncpgConfig
7+
from sqlspec.adapters.sqlite import SqliteConfig
8+
9+
__all__ = ("test_multi_database_setup_example",)
10+
11+
12+
async def test_multi_database_setup_example(tmp_path: Path, postgres_service: PostgresService) -> None:
13+
user_sql_path_pg = tmp_path / "sql" / "postgres"
14+
user_sql_path_pg.mkdir(parents=True, exist_ok=True)
15+
user_sql_file_pg = user_sql_path_pg / "users.sql"
16+
user_sql_file_pg.write_text(
17+
"""-- name: upsert_user
18+
INSERT INTO users_sf1 (id, username, email) VALUES (:id, :username, :email)
19+
ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username, email = EXCLUDED.email;
20+
"""
21+
)
22+
user_sql_path_sqlite = tmp_path / "sql" / "sqlite"
23+
user_sql_path_sqlite.mkdir(parents=True, exist_ok=True)
24+
user_sql_file_sqlite = user_sql_path_sqlite / "users.sql"
25+
user_sql_file_sqlite.write_text(
26+
"""-- name: get_user
27+
SELECT id, username, email FROM users_sf1 WHERE id = :user_id;
28+
"""
29+
)
30+
shared_sql_path = tmp_path / "sql" / "shared"
31+
shared_sql_path.mkdir(parents=True, exist_ok=True)
32+
shared_sql_file = shared_sql_path / "common.sql"
33+
shared_sql_file.write_text(
34+
"""-- name: delete_user
35+
DELETE FROM users_sf1 WHERE id = :user_id;
36+
"""
37+
)
38+
params = {"id": 1, "username": "john_doe", "email": "[email protected]"}
39+
40+
# start-example
41+
# Different SQL files for different databases
42+
loader = SQLFileLoader()
43+
loader.load_sql(tmp_path / "sql/postgres/", tmp_path / "sql/sqlite/", tmp_path / "sql/shared/")
44+
45+
# Queries automatically select correct dialect
46+
pg_query = loader.get_sql("upsert_user") # Uses Postgres ON CONFLICT
47+
sqlite_query = loader.get_sql("get_user") # Uses shared query
48+
49+
from sqlspec import SQLSpec
50+
51+
spec = SQLSpec()
52+
postgres_config = AsyncpgConfig(
53+
pool_config={
54+
"user": postgres_service.user,
55+
"password": postgres_service.password,
56+
"host": postgres_service.host,
57+
"port": postgres_service.port,
58+
"database": postgres_service.database,
59+
}
60+
)
61+
sqlite_config = SqliteConfig()
62+
# Execute on appropriate database
63+
async with spec.provide_session(postgres_config) as pg_session:
64+
await pg_session.execute("""CREATE TABLE users_sf1 ( id INTEGER PRIMARY KEY, username TEXT, email TEXT)""")
65+
await pg_session.execute(
66+
""" INSERT INTO users_sf1 (id, username, email) VALUES (1, 'old_name', '[email protected]');"""
67+
)
68+
69+
await pg_session.execute(pg_query, **params)
70+
71+
with spec.provide_session(sqlite_config) as sqlite_session:
72+
sqlite_session.execute("""CREATE TABLE users_sf1 ( id INTEGER PRIMARY KEY, username TEXT, email TEXT)""")
73+
sqlite_session.execute(
74+
""" INSERT INTO users_sf1 (id, username, email) VALUES (1, 'john_doe', '[email protected]');"""
75+
)
76+
sqlite_session.execute(sqlite_query, user_id=1)
77+
# end-example
78+
# Dummy asserts for doc example
79+
assert hasattr(loader, "load_sql")
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pathlib import Path
2+
3+
from docs.examples.usage.usage_sql_files_1 import create_loader
4+
from sqlspec.exceptions import SQLFileNotFoundError
5+
6+
__all__ = ("test_query_not_found",)
7+
8+
9+
def test_query_not_found(tmp_path: Path) -> None:
10+
loader, _queries = create_loader(tmp_path)
11+
# start-example
12+
try:
13+
loader.get_sql("nonexistent_query")
14+
except SQLFileNotFoundError:
15+
print("Query not found. Available queries:")
16+
print(loader.list_queries())
17+
# end-example
18+
# Dummy asserts for doc example
19+
assert hasattr(loader, "get_sql")
20+
assert hasattr(loader, "list_queries")
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from pathlib import Path
2+
3+
from docs.examples.usage.usage_sql_files_1 import create_loader
4+
5+
__all__ = ("test_file_load_errors",)
6+
7+
8+
def test_file_load_errors(tmp_path: Path) -> None:
9+
loader, _queries = create_loader(tmp_path)
10+
# start-example
11+
from sqlspec.exceptions import SQLFileNotFoundError, SQLFileParseError
12+
13+
try:
14+
loader.load_sql("sql/queries.sql")
15+
except SQLFileNotFoundError as e:
16+
print(f"File not found: {e}")
17+
except SQLFileParseError as e:
18+
print(f"Failed to parse SQL file: {e}")
19+
# end-example
20+
# Dummy asserts for doc example
21+
assert hasattr(loader, "load_sql")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from pathlib import Path
2+
3+
from docs.examples.usage.usage_sql_files_1 import create_loader
4+
5+
__all__ = ("test_debugging_loaded_queries",)
6+
7+
8+
def test_debugging_loaded_queries(tmp_path: Path) -> None:
9+
loader, _queries = create_loader(tmp_path)
10+
# start-example
11+
# Print query SQL
12+
query = loader.get_sql("create_user")
13+
print(f"SQL: {query}")
14+
print(f"Parameters: {query.parameters}")
15+
16+
# Inspect file metadata
17+
file_info = loader.get_file_for_query("create_user")
18+
print(f"Loaded from: {file_info.path}")
19+
# end-example
20+
# Dummy asserts for doc example
21+
assert hasattr(loader, "get_sql")
22+
assert query is not None

0 commit comments

Comments
 (0)