Skip to content

Commit abca00d

Browse files
authored
chore: readme update (#66)
Revise the README to align with current state
1 parent 9fa87fd commit abca00d

File tree

2 files changed

+458
-86
lines changed

2 files changed

+458
-86
lines changed

README.md

Lines changed: 229 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,21 @@ These are just a few examples that demonstrate SQLSpec's flexibility. Each of th
4141
```python
4242
from sqlspec import SQLSpec
4343
from sqlspec.adapters.sqlite import SqliteConfig
44-
from pydantic import BaseModel
44+
4545
# Create SQLSpec instance and configure database
46-
sql = SQLSpec()
47-
config = sql.add_config(SqliteConfig(database=":memory:"))
46+
db_manager = SQLSpec()
47+
config = SqliteConfig(pool_config={"database": ":memory:"}) # Thread local pooling
48+
db_manager.add_config(config)
4849

4950
# Execute queries with automatic result mapping
50-
with sql.provide_session(config) as session:
51+
with db_manager.provide_session(config) as session:
5152
# Simple query
5253
result = session.execute("SELECT 'Hello, SQLSpec!' as message")
5354
print(result.get_first()) # {'message': 'Hello, SQLSpec!'}
55+
56+
# Type-safe single row query
57+
row = session.select_one("SELECT 'Hello, SQLSpec!' as message")
58+
print(row) # {'message': 'Hello, SQLSpec!'}
5459
```
5560

5661
### SQL Builder Example (Experimental)
@@ -61,30 +66,94 @@ with sql.provide_session(config) as session:
6166
from sqlspec import sql
6267

6368
# Build a simple query
64-
query = sql.select("id", "name", "email").from_("users").where("active = ?", True)
65-
print(query.build().sql) # SELECT id, name, email FROM users WHERE active = ?
69+
query = sql.select("id", "name", "email").from_("users").where("active = ?")
70+
statement = query.to_statement()
71+
print(statement.sql) # SELECT id, name, email FROM users WHERE active = ?
6672

6773
# More complex example with joins
6874
query = (
6975
sql.select("u.name", "COUNT(o.id) as order_count")
7076
.from_("users u")
7177
.left_join("orders o", "u.id = o.user_id")
72-
.where("u.created_at > ?", "2024-01-01")
78+
.where("u.created_at > ?")
7379
.group_by("u.name")
74-
.having("COUNT(o.id) > ?", 5)
80+
.having("COUNT(o.id) > ?")
7581
.order_by("order_count", desc=True)
7682
)
7783

78-
# Execute the built query
79-
with sql.provide_session(config) as session:
80-
results = session.execute(query.build())
84+
# Execute the built query with parameters
85+
with db_manager.provide_session(config) as session:
86+
results = session.execute(query, "2024-01-01", 5)
87+
```
88+
89+
### Type-Safe Result Mapping
90+
91+
SQLSpec supports automatic mapping to typed models using popular libraries:
92+
93+
```python
94+
from sqlspec import SQLSpec
95+
from sqlspec.adapters.sqlite import SqliteConfig
96+
from pydantic import BaseModel
97+
98+
class User(BaseModel):
99+
id: int
100+
name: str
101+
email: str
102+
103+
db_manager = SQLSpec()
104+
config = SqliteConfig(pool_config={"database": ":memory:"})
105+
db_manager.add_config(config)
106+
107+
with db_manager.provide_session(config) as session:
108+
# Create and populate test data
109+
session.execute_script("""
110+
CREATE TABLE users (id INTEGER, name TEXT, email TEXT);
111+
INSERT INTO users VALUES (1, 'Alice', '[email protected]');
112+
""")
113+
# Map single result to typed model
114+
user = session.select_one("SELECT * FROM users WHERE id = ?", 1, schema_type=User)
115+
print(f"User: {user.name} ({user.email})")
116+
117+
# Map multiple results
118+
users = session.select("SELECT * FROM users", schema_type=User)
119+
for user in users:
120+
print(f"User: {user.name}")
121+
```
122+
123+
### Session Methods Overview
124+
125+
SQLSpec provides several convenient methods for executing queries:
126+
127+
```python
128+
with db_manager.provide_session(config) as session:
129+
# Execute any SQL and get full result set
130+
result = session.execute("SELECT * FROM users")
131+
132+
# Get single row (raises error if not found)
133+
user = session.select_one("SELECT * FROM users WHERE id = ?", 1)
134+
135+
# Get single row or None (no error if not found)
136+
maybe_user = session.select_one_or_none("SELECT * FROM users WHERE id = ?", 999)
137+
138+
# Execute with many parameter sets (bulk operations)
139+
session.execute_many(
140+
"INSERT INTO users (name, email) VALUES (?, ?)",
141+
[("Bob", "[email protected]"), ("Carol", "[email protected]")]
142+
)
143+
144+
# Execute multiple statements as a script
145+
session.execute_script("""
146+
CREATE TABLE IF NOT EXISTS logs (id INTEGER, message TEXT);
147+
INSERT INTO logs (message) VALUES ('System started');
148+
""")
81149
```
82150

83-
### DuckDB LLM
151+
<details>
152+
<summary>🦆 DuckDB LLM Integration Example</summary>
84153

85154
This is a quick implementation using some of the built-in Secret and Extension management features of SQLSpec's DuckDB integration.
86155

87-
It allows you to communicate with any compatible OpenAPI conversations endpoint (such as Ollama). This example:
156+
It allows you to communicate with any compatible OpenAI conversations endpoint (such as Ollama). This example:
88157

89158
- auto installs the `open_prompt` DuckDB extensions
90159
- automatically creates the correct `open_prompt` compatible secret required to use the extension
@@ -104,11 +173,12 @@ from pydantic import BaseModel
104173
class ChatMessage(BaseModel):
105174
message: str
106175

107-
sql = SQLSpec()
108-
etl_config = sql.add_config(
109-
DuckDBConfig(
110-
extensions=[{"name": "open_prompt"}],
111-
secrets=[
176+
db_manager = SQLSpec()
177+
config = DuckDBConfig(
178+
pool_config={"database": ":memory:"},
179+
driver_features={
180+
"extensions": [{"name": "open_prompt"}],
181+
"secrets": [
112182
{
113183
"secret_type": "open_prompt",
114184
"name": "open_prompt",
@@ -119,9 +189,11 @@ etl_config = sql.add_config(
119189
},
120190
}
121191
],
122-
)
192+
},
123193
)
124-
with sql.provide_session(etl_config) as session:
194+
db_manager.add_config(config)
195+
196+
with db_manager.provide_session(config) as session:
125197
result = session.select_one(
126198
"SELECT open_prompt(?)",
127199
"Can you write a haiku about DuckDB?",
@@ -130,7 +202,10 @@ with sql.provide_session(etl_config) as session:
130202
print(result) # result is a ChatMessage pydantic model
131203
```
132204

133-
### DuckDB Gemini Embeddings
205+
</details>
206+
207+
<details>
208+
<summary>🔗 DuckDB Gemini Embeddings Example</summary>
134209

135210
In this example, we are again using DuckDB. However, we are going to use the built-in to call the Google Gemini embeddings service directly from the database.
136211

@@ -157,11 +232,12 @@ API_URL = (
157232
f"https://generativelanguage.googleapis.com/v1beta/models/{EMBEDDING_MODEL}:embedContent?key=${GOOGLE_API_KEY}"
158233
)
159234

160-
sql = SQLSpec()
161-
etl_config = sql.add_config(
162-
DuckDBConfig(
163-
extensions=[{"name": "vss"}, {"name": "http_client"}],
164-
on_connection_create=lambda connection: connection.execute(f"""
235+
db_manager = SQLSpec()
236+
config = DuckDBConfig(
237+
pool_config={"database": ":memory:"},
238+
driver_features={
239+
"extensions": [{"name": "vss"}, {"name": "http_client"}],
240+
"on_connection_create": lambda connection: connection.execute(f"""
165241
CREATE IF NOT EXISTS MACRO generate_embedding(q) AS (
166242
WITH __request AS (
167243
SELECT http_post(
@@ -180,16 +256,77 @@ etl_config = sql.add_config(
180256
FROM __request,
181257
);
182258
"""),
183-
)
259+
},
184260
)
185-
with sql.provide_session(etl_config) as session:
261+
db_manager.add_config(config)
262+
263+
with db_manager.provide_session(config) as session:
186264
result = session.execute("SELECT generate_embedding('example text')")
187265
print(result.get_first()) # result is a dictionary when `schema_type` is omitted.
188266
```
189267

268+
</details>
269+
270+
### SQL File Loading
271+
272+
SQLSpec can load and manage SQL queries from files using aiosql-style named queries:
273+
274+
```python
275+
from sqlspec import SQLSpec
276+
from sqlspec.loader import SQLFileLoader
277+
from sqlspec.adapters.sqlite import SqliteConfig
278+
279+
# Initialize with SQL file loader
280+
db_manager = SQLSpec(loader=SQLFileLoader())
281+
config = SqliteConfig(pool_config={"database": ":memory:"})
282+
db_manager.add_config(config)
283+
284+
# Load SQL files from directory
285+
db_manager.load_sql_files("./sql")
286+
287+
# SQL file: ./sql/users.sql
288+
# -- name: get_user
289+
# SELECT * FROM users WHERE id = ?
290+
#
291+
# -- name: create_user
292+
# INSERT INTO users (name, email) VALUES (?, ?)
293+
294+
with db_manager.provide_session(config) as session:
295+
# Use named queries from files
296+
user = session.execute(db_manager.get_sql("get_user"), 1)
297+
session.execute(db_manager.get_sql("create_user"), "Alice", "[email protected]")
298+
```
299+
300+
### Database Migrations
301+
302+
SQLSpec includes a built-in migration system for managing schema changes. After configuring your database with migration settings, use the CLI commands:
303+
304+
```bash
305+
# Initialize migration directory
306+
sqlspec db init migrations
307+
308+
# Generate new migration file
309+
sqlspec db make-migrations "Add user table"
310+
311+
# Apply all pending migrations
312+
sqlspec db upgrade
313+
314+
# Show current migration status
315+
sqlspec db show-current-revision
316+
```
317+
318+
For Litestar applications, replace `sqlspec` with your application command:
319+
320+
```bash
321+
# Using Litestar CLI integration
322+
litestar db make-migrations "Add user table"
323+
litestar db upgrade
324+
litestar db show-current-revision
325+
```
326+
190327
### Basic Litestar Integration
191328

192-
In this example we are going to demonstrate how to create a basic configuration that integrates into Litestar.
329+
In this example we demonstrate how to create a basic configuration that integrates into Litestar:
193330

194331
```py
195332
# /// script
@@ -212,7 +349,7 @@ async def simple_sqlite(db_session: AiosqliteDriver) -> dict[str, str]:
212349

213350
sqlspec = SQLSpec(
214351
config=DatabaseConfig(
215-
config=AiosqliteConfig(),
352+
config=AiosqliteConfig(pool_config={"database": ":memory:"}), # built in local pooling
216353
commit_mode="autocommit"
217354
)
218355
)
@@ -231,6 +368,41 @@ The primary goal at this stage is to establish a **native connectivity interface
231368

232369
This list is not final. If you have a driver you'd like to see added, please open an issue or submit a PR!
233370

371+
### Configuration Examples
372+
373+
Each adapter uses a consistent configuration pattern with `pool_config` for connection parameters:
374+
375+
```python
376+
# SQLite
377+
SqliteConfig(pool_config={"database": "/path/to/database.db"})
378+
AiosqliteConfig(pool_config={"database": "/path/to/database.db"}) # Async
379+
AdbcConfig(connection_config={"uri": "sqlite:///path/to/database.db"}) # ADBC
380+
381+
# PostgreSQL (multiple drivers available)
382+
PsycopgSyncConfig(pool_config={"host": "localhost", "database": "mydb", "user": "user", "password": "pass"})
383+
PsycopgAsyncConfig(pool_config={"host": "localhost", "database": "mydb", "user": "user", "password": "pass"}) # Async
384+
AsyncpgConfig(pool_config={"host": "localhost", "database": "mydb", "user": "user", "password": "pass"})
385+
PsqlpyConfig(pool_config={"dsn": "postgresql://user:pass@localhost/mydb"})
386+
AdbcConfig(connection_config={"uri": "postgresql://user:pass@localhost/mydb"}) # ADBC
387+
388+
# DuckDB
389+
DuckDBConfig(pool_config={"database": ":memory:"}) # or file path
390+
AdbcConfig(connection_config={"uri": "duckdb:///path/to/database.duckdb"}) # ADBC
391+
392+
# MySQL
393+
AsyncmyConfig(pool_config={"host": "localhost", "database": "mydb", "user": "user", "password": "pass"}) # Async
394+
395+
# Oracle
396+
OracleSyncConfig(pool_config={"host": "localhost", "service_name": "XEPDB1", "user": "user", "password": "pass"})
397+
OracleAsyncConfig(pool_config={"host": "localhost", "service_name": "XEPDB1", "user": "user", "password": "pass"}) # Async
398+
399+
# BigQuery
400+
BigQueryConfig(pool_config={"project": "my-project", "dataset": "my_dataset"})
401+
AdbcConfig(connection_config={"driver_name": "adbc_driver_bigquery", "project_id": "my-project", "dataset_id": "my_dataset"}) # ADBC
402+
```
403+
404+
### Supported Drivers
405+
234406
| Driver | Database | Mode | Status |
235407
| :----------------------------------------------------------------------------------------------------------- | :--------- | :------ | :--------- |
236408
| [`adbc`](https://arrow.apache.org/adbc/) | Postgres | Sync ||
@@ -253,21 +425,35 @@ This list is not final. If you have a driver you'd like to see added, please ope
253425
| [`asyncmy`](https://github.com/long2ice/asyncmy) | MySQL | Async ||
254426
| [`snowflake`](https://docs.snowflake.com) | Snowflake | Sync | 🗓️ |
255427

256-
## Proposed Project Structure
428+
## Project Structure
257429

258430
- `sqlspec/`:
259-
- `adapters/`: Contains all database drivers and associated configuration.
260-
- `extensions/`:
261-
- `litestar/`: Litestar framework integration ✅
262-
- `fastapi/`: Future home of `fastapi` integration.
263-
- `flask/`: Future home of `flask` integration.
264-
- `*/`: Future home of your favorite framework integration
265-
- `base.py`: Contains base protocols for database configurations.
266-
- `statement/`: Contains the SQL statement system with builders, validation, and transformation.
267-
- `storage/`: Contains unified storage operations for data import/export.
268-
- `utils/`: Contains utility functions used throughout the project.
269-
- `exceptions.py`: Contains custom exceptions for SQLSpec.
270-
- `typing.py`: Contains type hints, type guards and several facades for optional libraries that are not required for the core functionality of SQLSpec.
431+
- `adapters/`: Database-specific drivers and configuration classes for all supported databases
432+
- `extensions/`: Framework integrations and external library adapters
433+
- `litestar/`: Litestar web framework integration with dependency injection ✅
434+
- `aiosql/`: Integration with aiosql for SQL file loading ✅
435+
- Future integrations: `fastapi/`, `flask/`, etc.
436+
- `builder/`: Fluent SQL query builder with method chaining and type safety
437+
- `mixins/`: Composable query building operations (WHERE, JOIN, ORDER BY, etc.)
438+
- `core/`: Core query processing infrastructure
439+
- `statement.py`: SQL statement wrapper with metadata and type information
440+
- `parameters.py`: Parameter style conversion and validation
441+
- `result.py`: Result set handling and type mapping
442+
- `compiler.py`: SQL compilation and validation using SQLGlot
443+
- `cache.py`: Statement caching for performance optimization
444+
- `driver/`: Base driver system with sync/async support and transaction management
445+
- `mixins/`: Shared driver capabilities (result processing, SQL translation)
446+
- `migrations/`: Database migration system with CLI commands
447+
- `storage/`: Unified data import/export operations with multiple backends
448+
- `backends/`: Storage backend implementations (fsspec, obstore)
449+
- `utils/`: Utility functions, type guards, and helper tools
450+
- `base.py`: Main SQLSpec registry and configuration manager
451+
- `loader.py`: SQL file loading system for `.sql` files
452+
- `cli.py`: Command-line interface for migrations and database operations
453+
- `config.py`: Base configuration classes and protocols
454+
- `protocols.py`: Type protocols for runtime type checking
455+
- `exceptions.py`: Custom exception hierarchy for SQLSpec
456+
- `typing.py`: Type definitions, guards, and optional dependency facades
271457

272458
## Get Involved
273459

0 commit comments

Comments
 (0)