Skip to content

Commit 6aab462

Browse files
authored
docs: enhance framework examples and SQL playground (#343)
- Fix framework documentation with correct driver types and `extension_config` patterns - Add multi-database examples for FastAPI, Flask, and Starlette showing async+sync database configurations - Enhance SQL playground with sortable datatable, blinking cursor, improved table header styling - Fix `sphinx_datatables` extension error by ensuring `_static` directory exists before build - Update Litestar dependency injection docs with DuckDB advanced configuration patterns ## Changes ### Framework Examples - **FastAPI**: Fixed type annotations, removed `from __future__ import annotations` (causes FastAPI inspection issues), added multi-database example - **Flask**: Added factory pattern with `init_app()`, added multi-database example - **Starlette**: Clarified session access patterns, added multi-database example - **Litestar**: Enhanced dependency injection docs with proper `extension_config` usage and DuckDB hooks ### SQL Playground - Added sortable columns with sticky headers - Added blinking golden cursor for visibility - Improved table header styling (light/dark mode aware) - Combined SQL statements using `execute_script()` - Moved status indicator to left of buttons
1 parent 7e8af3e commit 6aab462

11 files changed

Lines changed: 543 additions & 90 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,6 @@ specs/
6161
.playwright-mcp
6262
.geminiignore
6363
uv.toml
64-
.beads/
6564
.agent/
65+
.geminiignore
66+
.beads/

docs/conf.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,18 @@ def update_html_context(
320320
context["generate_toctree_html"] = partial(context["generate_toctree_html"], startdepth=0)
321321

322322

323+
def _ensure_static_dir(app: Sphinx, exception: Any) -> None:
324+
"""Ensure _static directory exists for extensions that write to it."""
325+
if exception is None and hasattr(app.builder, "outdir"):
326+
from pathlib import Path
327+
328+
static_dir = Path(app.builder.outdir) / "_static"
329+
static_dir.mkdir(parents=True, exist_ok=True)
330+
331+
323332
def setup(app: Sphinx) -> dict[str, bool]:
324333
app.setup_extension("shibuya")
334+
# Ensure _static exists before sphinx_datatables tries to write to it
335+
# Use priority < 500 to run before sphinx_datatables' finish handler
336+
app.connect("build-finished", _ensure_static_dir, priority=100)
325337
return {"parallel_read_safe": True, "parallel_write_safe": True}

docs/examples/extensions/litestar/plugin_setup.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
def test_litestar_plugin_setup() -> None:
99
pytest.importorskip("litestar")
1010
# start-example
11-
from litestar import Litestar
11+
from litestar import Litestar, get
1212

1313
from sqlspec import SQLSpec
14-
from sqlspec.adapters.sqlite import SqliteConfig
14+
from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver
1515
from sqlspec.extensions.litestar import SQLSpecPlugin
1616

1717
sqlspec = SQLSpec()
@@ -21,7 +21,12 @@ def test_litestar_plugin_setup() -> None:
2121
)
2222
)
2323

24-
app = Litestar(plugins=[SQLSpecPlugin(sqlspec=sqlspec)])
24+
@get("/health")
25+
def health_check(db_session: SqliteDriver) -> dict[str, str]:
26+
result = db_session.execute("SELECT 'ok' as status")
27+
return result.one()
28+
29+
app = Litestar(route_handlers=[health_check], plugins=[SQLSpecPlugin(sqlspec=sqlspec)])
2530
# end-example
2631

2732
assert app is not None

docs/examples/frameworks/fastapi/basic_setup.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from __future__ import annotations
2-
31
from typing import Annotated, Any
42

53
import pytest
@@ -14,7 +12,7 @@ def test_fastapi_basic_setup() -> None:
1412
from fastapi import Depends, FastAPI
1513

1614
from sqlspec import SQLSpec
17-
from sqlspec.adapters.aiosqlite import AiosqliteConfig
15+
from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver
1816
from sqlspec.extensions.fastapi import SQLSpecPlugin
1917

2018
sqlspec = SQLSpec()
@@ -24,7 +22,7 @@ def test_fastapi_basic_setup() -> None:
2422
db_ext = SQLSpecPlugin(sqlspec, app)
2523

2624
@app.get("/teams")
27-
async def list_teams(db: Annotated[Any, Depends(db_ext.provide_session())]) -> Any:
25+
async def list_teams(db: Annotated[AiosqliteDriver, Depends(db_ext.provide_session())]) -> dict[str, Any]:
2826
result = await db.execute("select 1 as ok")
2927
return result.one()
3028

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from typing import Annotated
2+
3+
import pytest
4+
5+
__all__ = ("test_fastapi_multi_database",)
6+
7+
8+
def test_fastapi_multi_database() -> None:
9+
pytest.importorskip("fastapi")
10+
pytest.importorskip("aiosqlite")
11+
# start-example
12+
from fastapi import Depends, FastAPI
13+
14+
from sqlspec import SQLSpec
15+
from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver
16+
from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver
17+
from sqlspec.extensions.fastapi import SQLSpecPlugin
18+
19+
sqlspec = SQLSpec()
20+
21+
# Primary async database
22+
sqlspec.add_config(
23+
AiosqliteConfig(
24+
connection_config={"database": ":memory:"},
25+
extension_config={
26+
"starlette": {"session_key": "db", "connection_key": "db_connection", "pool_key": "db_pool"}
27+
},
28+
)
29+
)
30+
31+
# ETL sync database (e.g., DuckDB pattern)
32+
sqlspec.add_config(
33+
SqliteConfig(
34+
connection_config={"database": ":memory:"},
35+
extension_config={
36+
"starlette": {"session_key": "etl_db", "connection_key": "etl_connection", "pool_key": "etl_pool"}
37+
},
38+
)
39+
)
40+
41+
app = FastAPI()
42+
db_plugin = SQLSpecPlugin(sqlspec, app)
43+
44+
@app.get("/report")
45+
async def report(
46+
db: Annotated[AiosqliteDriver, Depends(db_plugin.provide_session("db"))],
47+
etl_db: Annotated[SqliteDriver, Depends(db_plugin.provide_session("etl_db"))],
48+
) -> dict[str, list]:
49+
# Async query to primary database
50+
users = await db.select("SELECT 1 as id, 'Alice' as name")
51+
# Sync query to ETL database
52+
metrics = etl_db.select("SELECT 'metric1' as name, 100 as value")
53+
return {"users": users, "metrics": metrics}
54+
55+
# end-example
56+
57+
assert app is not None
Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from __future__ import annotations
22

3-
from typing import Any
4-
53
import pytest
64

75
__all__ = ("test_flask_basic_setup",)
@@ -13,21 +11,29 @@ def test_flask_basic_setup() -> None:
1311
from flask import Flask
1412

1513
from sqlspec import SQLSpec
16-
from sqlspec.adapters.sqlite import SqliteConfig
14+
from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver
1715
from sqlspec.extensions.flask import SQLSpecPlugin
1816

17+
# Create SQLSpec and plugin at module level
1918
sqlspec = SQLSpec()
2019
sqlspec.add_config(SqliteConfig(connection_config={"database": ":memory:"}))
20+
plugin = SQLSpecPlugin(sqlspec)
21+
22+
def create_app() -> Flask:
23+
"""Application factory pattern."""
24+
app = Flask(__name__)
25+
plugin.init_app(app)
2126

22-
app = Flask(__name__)
23-
plugin = SQLSpecPlugin(sqlspec, app)
27+
@app.get("/health")
28+
def health() -> dict[str, int]:
29+
db: SqliteDriver = plugin.get_session()
30+
result = db.execute("select 1 as ok")
31+
return result.one()
2432

25-
@app.get("/health")
26-
def health() -> Any:
27-
session = plugin.get_session()
28-
result = session.execute("select 1 as ok")
29-
return result.one()
33+
return app
3034

35+
app = create_app()
3136
# end-example
3237

3338
assert plugin is not None
39+
assert app is not None
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
__all__ = ("test_flask_multi_database",)
6+
7+
8+
def test_flask_multi_database() -> None:
9+
pytest.importorskip("flask")
10+
# start-example
11+
from flask import Flask
12+
13+
from sqlspec import SQLSpec
14+
from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver
15+
from sqlspec.extensions.flask import SQLSpecPlugin
16+
17+
sqlspec = SQLSpec()
18+
19+
# Primary database
20+
sqlspec.add_config(
21+
SqliteConfig(
22+
connection_config={"database": ":memory:"},
23+
extension_config={"flask": {"session_key": "db", "connection_key": "db_connection", "pool_key": "db_pool"}},
24+
)
25+
)
26+
27+
# ETL database with custom keys
28+
sqlspec.add_config(
29+
SqliteConfig(
30+
connection_config={"database": ":memory:"},
31+
extension_config={
32+
"flask": {"session_key": "etl_db", "connection_key": "etl_connection", "pool_key": "etl_pool"}
33+
},
34+
)
35+
)
36+
37+
plugin = SQLSpecPlugin(sqlspec)
38+
39+
def create_app() -> Flask:
40+
app = Flask(__name__)
41+
plugin.init_app(app)
42+
43+
@app.get("/report")
44+
def report() -> dict[str, list]:
45+
db: SqliteDriver = plugin.get_session("db")
46+
etl_db: SqliteDriver = plugin.get_session("etl_db")
47+
48+
users = db.select("SELECT 1 as id, 'Alice' as name")
49+
metrics = etl_db.select("SELECT 'metric1' as name, 100 as value")
50+
return {"users": users, "metrics": metrics}
51+
52+
return app
53+
54+
app = create_app()
55+
# end-example
56+
57+
assert app is not None

docs/examples/frameworks/starlette/basic_setup.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,22 @@ def test_starlette_basic_setup() -> None:
1515
from starlette.routing import Route
1616

1717
from sqlspec import SQLSpec
18-
from sqlspec.adapters.aiosqlite import AiosqliteConfig
18+
from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver
1919
from sqlspec.extensions.starlette import SQLSpecPlugin
2020

2121
sqlspec = SQLSpec()
2222
sqlspec.add_config(AiosqliteConfig(connection_config={"database": ":memory:"}))
2323

24+
# Create plugin at module level
25+
db_plugin = SQLSpecPlugin(sqlspec)
26+
2427
async def health(request: Request) -> JSONResponse:
25-
db = request.app.state.sqlspec.get_session(request)
28+
db: AiosqliteDriver = db_plugin.get_session(request)
2629
result = await db.execute("select 1 as ok")
2730
return JSONResponse(result.one())
2831

2932
app = Starlette(routes=[Route("/health", health)])
30-
app.state.sqlspec = SQLSpecPlugin(sqlspec, app)
33+
db_plugin.init_app(app) # Initialize plugin with app
3134
# end-example
3235

3336
assert app is not None
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
__all__ = ("test_starlette_multi_database",)
6+
7+
8+
def test_starlette_multi_database() -> None:
9+
pytest.importorskip("starlette")
10+
pytest.importorskip("aiosqlite")
11+
# start-example
12+
from starlette.applications import Starlette
13+
from starlette.requests import Request
14+
from starlette.responses import JSONResponse
15+
from starlette.routing import Route
16+
17+
from sqlspec import SQLSpec
18+
from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver
19+
from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver
20+
from sqlspec.extensions.starlette import SQLSpecPlugin
21+
22+
sqlspec = SQLSpec()
23+
24+
# Primary async database
25+
sqlspec.add_config(
26+
AiosqliteConfig(
27+
connection_config={"database": ":memory:"},
28+
extension_config={
29+
"starlette": {"session_key": "db", "connection_key": "db_connection", "pool_key": "db_pool"}
30+
},
31+
)
32+
)
33+
34+
# ETL sync database
35+
sqlspec.add_config(
36+
SqliteConfig(
37+
connection_config={"database": ":memory:"},
38+
extension_config={
39+
"starlette": {"session_key": "etl_db", "connection_key": "etl_connection", "pool_key": "etl_pool"}
40+
},
41+
)
42+
)
43+
44+
db_plugin = SQLSpecPlugin(sqlspec)
45+
46+
async def report(request: Request) -> JSONResponse:
47+
db: AiosqliteDriver = db_plugin.get_session(request, "db")
48+
etl_db: SqliteDriver = db_plugin.get_session(request, "etl_db")
49+
50+
# Async query to primary database
51+
users = await db.select("SELECT 1 as id, 'Alice' as name")
52+
# Sync query to ETL database
53+
metrics = etl_db.select("SELECT 'metric1' as name, 100 as value")
54+
return JSONResponse({"users": users, "metrics": metrics})
55+
56+
app = Starlette(routes=[Route("/report", report)])
57+
db_plugin.init_app(app)
58+
# end-example
59+
60+
assert app is not None

0 commit comments

Comments
 (0)