Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ through a language frontend, semantic binding, and pluggable backend
lowering - making it easy to expose complex query capabilities in your API
without coupling to a specific ORM or framework.

For FastAPI applications using SQLAlchemy, the recommended entry point is
`FastAPISQLAlchemyIntegration`. It provides route-ready dependencies and
declarative resources; the lower-level query, adapter, and ORM APIs remain
available when more control is needed.

**Current backends:** SQLAlchemy 2.0
**Current framework adapters:** FastAPI
**Planned:** Django ORM, SQLModel, Flask
Expand Down Expand Up @@ -38,9 +43,41 @@ language.
## Quickstart

```bash
pip install pyrsql[sqlalchemy]
pip install pyrsql[fastapi,sqlalchemy]
```

```python
from typing import Annotated, Any

from fastapi import Depends, FastAPI

from pyrsql.integrations.fastapi import FastAPISQLAlchemyIntegration

app = FastAPI()
integration = FastAPISQLAlchemyIntegration()
users = integration.resource(
User,
filterable_fields={"id", "name"},
sortable_fields={"name"},
default_sort="name,asc",
max_page_size=100,
)

@app.get("/users")
def list_users(
stmt: Annotated[Any, Depends(users.select_dependency())],
):
return {"sql": str(stmt)}
```

Clients can now use `filter`, `sort`, `page`, and `size` query parameters.
See [FastAPI + SQLAlchemy](https://wskr00.github.io/pyrsql/usage/fastapi-sqlalchemy/)
for count, pagination, custom base statements, error behavior, and async use.

### Lower-level SQLAlchemy API

Use the direct API when you do not need a FastAPI route dependency:

```python
import pyrsql
from sqlalchemy import select
Expand All @@ -49,13 +86,13 @@ from pyrsql.orms.sqlalchemy import SQLAlchemyORM
orm = SQLAlchemyORM()
stmt = select(User)

# Filter: name equals "demo", company name contains "acme"
stmt = pyrsql.parse("name==demo;company.name==acme*").apply(stmt, User, orm=orm)
stmt = pyrsql.parse("name==demo;company.name==acme*").apply(
stmt,
User,
orm=orm,
)

# Sort: by name ascending, then company name descending
stmt = pyrsql.Sort.parse("name,asc;company.name,desc").apply(stmt, User, orm=orm)

# Paginate: page 0, 25 items per page
stmt = pyrsql.PageRequest.of(0, 25).apply(stmt, User, orm=orm)
```

Expand Down Expand Up @@ -133,6 +170,9 @@ options = QueryOptions(join_hints={"User.company": JoinHint.LEFT})

### FastAPI Integration

Use this adapter directly when you need only parsed `RequestCriteria`; for
SQLAlchemy routes, prefer the integration below.

```python
from fastapi import Depends, FastAPI
from pyrsql.adapters.fastapi import criteria_dependency
Expand All @@ -146,10 +186,11 @@ def list_items(criteria = Depends(dependency)):
```

- Auto-extracts `filter`, `sort`, `page`, `size` from query params
- `HTTP 422` with structured diagnostics on parse/semantic errors
- Structured `HTTP 400` parse errors and `HTTP 422` semantic errors
- OpenAPI examples from configuration
- One-based paging support
- Custom query parameter names
- Optional repeated sort parameters through `SortParameterFormat.REPEATED`

### FastAPI + SQLAlchemy Integration

Expand All @@ -164,15 +205,15 @@ def list_users(stmt = Depends(integration.select_dependency(User))):
```

- `select_dependency`, `count_select_dependency`, `paginated_select_dependency`
- Declarative `resource()` with auto-generated OpenAPI examples
- Declarative `resource()` with auto-generated OpenAPI examples and field policies
- `applier_dependency` for custom base statements
- Compatible with both sync `Session` and async `AsyncSession` execution
- FastAPI parse and page-validation failures become structured `HTTP 400` payloads
- FastAPI semantic and backend integration failures become structured `HTTP 422` payloads

### Concurrency and Validation

- Shared integration and ORM metadata caches are protected for free-threaded execution
- Shared base-select and ORM metadata caches are protected for free-threaded execution
- Async support is validated for adapter, ORM, and integration flows
- Dedicated async, free-threaded, and security test suites validate these flows

Expand Down
7 changes: 3 additions & 4 deletions docs/explanation/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ Owns user-facing ORM-agnostic objects:
- `JSONPath`, `JSONPathComparison`
- `JSONScalarNormalizer`, `JSONScalarValue`
- `JoinHint`
- `CompilationResult`, `SortCompilationResult`, `PageCompilationResult`

The `core/json/` package owns JSON-aware comparison models and value
normalization, keeping JSON semantics ORM-neutral.
Expand Down Expand Up @@ -185,9 +184,9 @@ Implement `pyrsql.orms.base.ORM`:

```python
class ORM(ABC):
def compile_query(self, query: Query) -> CompiledQuery: ...
def compile_sort(self, sort: Sort) -> CompiledSort: ...
def compile_page_request(self, page_request: PageRequest) -> CompiledPageRequest: ...
def compile_query(self, query: Query) -> CompiledArtifact: ...
def compile_sort(self, sort: Sort) -> CompiledArtifact: ...
def compile_page_request(self, page_request: PageRequest) -> CompiledArtifact: ...
```

Each method receives one ORM-neutral core object and returns a compiled object
Expand Down
26 changes: 9 additions & 17 deletions docs/explanation/extensibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,17 @@ The core (`pyrsql.core`, `pyrsql.parsing`, `pyrsql.semantic`,
Implement the `ORM` abstract base class:

```python
from pyrsql.orms.base import ORM, CompiledPageRequest, CompiledQuery, CompiledSort
from pyrsql.core.compiler import CompiledArtifact
from pyrsql.orms.base import ORM

class MyORM(ORM):
@property
def name(self) -> str:
return "my-orm"

def compile_query(self, query: Query) -> CompiledQuery:
def compile_query(self, query: Query) -> CompiledArtifact:
...

def compile_sort(self, sort: Sort) -> CompiledSort:
def compile_sort(self, sort: Sort) -> CompiledArtifact:
...

def compile_page_request(self, page_request: PageRequest) -> CompiledPageRequest:
def compile_page_request(self, page_request: PageRequest) -> CompiledArtifact:
...
```

Expand All @@ -49,23 +46,18 @@ The FastAPI adapter (`pyrsql.adapters.fastapi`) is the reference:

## Backend contract in detail

### `CompiledQuery`
### `CompiledArtifact`

```python
class CompiledQuery(Protocol):
class CompiledArtifact(Protocol):
def apply(self, target: _TargetT, model: type[_ModelT]) -> _TargetT: ...
```

Receives an ORM-specific target and model class, then returns the modified
target with query conditions applied.

### `CompiledSort`

Same contract as `CompiledQuery`, but applies sort ordering.

### `CompiledPageRequest`

Same contract, but applies pagination semantics such as `LIMIT` and `OFFSET`.
The same contract applies to query, sort, and pagination artifacts such as
`LIMIT` and `OFFSET`.

## Integration layer

Expand Down
36 changes: 27 additions & 9 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,38 @@ capabilities in your API without coupling to a specific ORM or framework.
## Install

```bash
pip install pyrsql[sqlalchemy]
pip install pyrsql[fastapi,sqlalchemy]
```

## First query
## Recommended: FastAPI + SQLAlchemy

```python
import pyrsql
from sqlalchemy import select
from pyrsql.orms.sqlalchemy import SQLAlchemyORM
from typing import Annotated, Any

orm = SQLAlchemyORM()
stmt = pyrsql.parse("name==demo;age=gt=18").apply(select(User), User, orm=orm)
from fastapi import Depends, FastAPI

from pyrsql.integrations.fastapi import FastAPISQLAlchemyIntegration

app = FastAPI()
integration = FastAPISQLAlchemyIntegration()
users = integration.resource(
User,
filterable_fields={"id", "name"},
sortable_fields={"name"},
default_sort="name,asc",
)

@app.get("/users")
def list_users(
stmt: Annotated[Any, Depends(users.select_dependency())],
):
return {"sql": str(stmt)}
```

The integration parses request criteria, applies SQLAlchemy filters/sorting/
pagination, and exposes a route-ready dependency. It does not execute database
I/O.

## Key features

- **ORM-neutral core** - `Query`, `Sort`, `PageRequest` have zero ORM dependencies
Expand All @@ -43,11 +61,11 @@ stmt = pyrsql.parse("name==demo;age=gt=18").apply(select(User), User, orm=orm)

## Integration options

- **FastAPI adapter** - parse `filter`, `sort`, `page`, and `size` into
`RequestCriteria`
- **FastAPI + SQLAlchemy integration** - route-ready dependencies,
declarative `resource()` helpers, count/paginated bundles, and normalized
backend error translation
- **FastAPI adapter** - parse `filter`, `sort`, `page`, and `size` into
`RequestCriteria` when you need criteria without SQLAlchemy integration

## Next steps

Expand Down
40 changes: 39 additions & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,44 @@ pip install pyrsql[fastapi] # with FastAPI
pip install pyrsql[fastapi,sqlalchemy] # both
```

## Recommended: FastAPI + SQLAlchemy

For a FastAPI application backed by SQLAlchemy, start with the integration.
It owns the public query contract for one model and gives routes a ready-to-use
SQLAlchemy statement.

```python
from typing import Annotated, Any

from fastapi import Depends, FastAPI

from pyrsql.integrations.fastapi import FastAPISQLAlchemyIntegration

app = FastAPI()
integration = FastAPISQLAlchemyIntegration()
users = integration.resource(
User,
filterable_fields={"id", "name"},
sortable_fields={"name"},
default_sort="name,asc",
)

@app.get("/users")
def list_users(
stmt: Annotated[Any, Depends(users.select_dependency())],
):
return {"sql": str(stmt)}
```

Clients can use `filter`, `sort`, `page`, and `size`. The integration also
offers count and paginated dependencies. See [FastAPI + SQLAlchemy](usage/fastapi-sqlalchemy.md)
for the complete route API.

## Building blocks

The following sections show the lower-level APIs used by the integration. They
are useful for non-FastAPI applications or when you need direct control.

## Filter

```python
Expand Down Expand Up @@ -83,7 +121,7 @@ Parse and page-validation errors become structured `HTTP 400` responses.
Semantic and backend integration errors become structured `HTTP 422`
responses.

## FastAPI + SQLAlchemy
## Direct integration dependency

```python
from typing import Annotated, Any
Expand Down
3 changes: 1 addition & 2 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,11 @@
- ValueConverterRegistry
- JoinHint
- ORM
- CompilationResult
- DEFAULT_JSON_OPTIONS

## Subpackages

- `pyrsql.adapters.fastapi` - FastAPI adapter (FastAPICriteriaConfig, RequestCriteria, CriteriaDependency)
- `pyrsql.adapters.fastapi` - FastAPI adapter (FastAPICriteriaConfig, RequestCriteria, CriteriaDependency, SortParameterFormat)
- `pyrsql.integrations.fastapi` - FastAPI + SQLAlchemy (FastAPISQLAlchemyIntegration, FastAPISQLAlchemyResource)
- `pyrsql.orms.sqlalchemy` - SQLAlchemy backend (SQLAlchemyORM)
- `pyrsql.core` - ORM-neutral core types and options
Expand Down
1 change: 0 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ shared mutable state.

In `pyrsql`, the main targets are:

- integration dependency caches
- integration base-statement caches
- SQLAlchemy model-introspection caches
- resolver caches
Expand Down
24 changes: 20 additions & 4 deletions docs/usage/fastapi-sqlalchemy.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# FastAPI + SQLAlchemy

## Setup
## Recommended entry point

```python
from pyrsql.integrations.fastapi import FastAPISQLAlchemyIntegration
Expand All @@ -21,6 +21,23 @@ The integration works with:
- synchronous SQLAlchemy execution through `Session`
- asynchronous SQLAlchemy execution through `AsyncSession`

For routes that expose one stable model contract, prefer a declarative
resource. It keeps field policies, default ordering, page limits, and OpenAPI
examples next to the model they describe.

```python
users = integration.resource(
User,
filterable_fields={"id", "name"},
sortable_fields={"name"},
default_sort="name,desc",
max_page_size=50,
)
```

Use `FastAPISQLAlchemyIntegration` directly for dynamic models or when a
service layer needs to build statements outside a route dependency.

## Route-ready dependencies

```python
Expand Down Expand Up @@ -140,9 +157,8 @@ This avoids leaking raw SQLAlchemy exceptions as `500` responses.

## Free-threaded behavior

The integration caches dependency callables and base `select(model)` statements
per model. Those shared caches are published with explicit synchronization for
free-threaded execution.
The integration caches base `select(model)` statements per model. That shared
cache is published with explicit synchronization for free-threaded execution.

## Async note

Expand Down
23 changes: 23 additions & 0 deletions docs/usage/fastapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ config = FastAPICriteriaConfig(
dependency = criteria_dependency(config)
```

## Repeated sort parameters

The default sort representation uses one semicolon-delimited parameter:

```text
?sort=name,asc;created_at,desc
```

To accept one sort field per repeated query parameter, configure the adapter
with `SortParameterFormat.REPEATED`:

```python
from pyrsql.adapters.fastapi import SortParameterFormat

config = FastAPICriteriaConfig(
sort_parameter_format=SortParameterFormat.REPEATED,
)
```

```text
?sort=name,asc&sort=created_at,desc
```

## Class-based dependency

```python
Expand Down
Loading
Loading