Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
Update docs for SQLAlchemy 2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jpsca committed Feb 16, 2023
1 parent a87bb03 commit ce09ad4
Show file tree
Hide file tree
Showing 10 changed files with 64 additions and 76 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
![SQLA-Wrapper](header.png)

A friendly wrapper for [modern SQLAlchemy](https://docs.sqlalchemy.org/en/14/glossary.html#term-2.0-style) (v1.4 or later) and Alembic.
A friendly wrapper for [modern SQLAlchemy](https://docs.sqlalchemy.org/en/20/glossary.html#term-2.0-style) (v1.4 or later) and Alembic.

**Documentation:** https://sqla-wrapper.scaletti.dev/

Includes:

- A SQLAlchemy wrapper, that does all the SQLAlchemy setup and gives you:
- A scoped session extended with some useful active-record-like methods and pagination helper.
- A scoped session extended with some useful active-record-like methods.
- A declarative base class.
- A helper for performant testing with a real database.

Expand Down
9 changes: 1 addition & 8 deletions docs/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

::: sqla_wrapper.Session
:docstring:
:members: all create first first_or_create create_or_first paginate
:members: all create first first_or_create create_or_first


## TestTransaction class
Expand All @@ -27,10 +27,3 @@
::: sqla_wrapper.Alembic
:docstring:
:members: revision upgrade downgrade get_history history stamp get_current current get_head head init create_all rev_id get_proper_cli get_click_cli get_flask_cli


## Paginator class

::: sqla_wrapper.Paginator
:docstring:
:members: num_pages total_pages showing is_paginated has_prev has_next next_num prev_num prev next start_index end_index items pages get_range get_pages
41 changes: 17 additions & 24 deletions docs/docs/how-to.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,25 @@
# How to ...?

In this section you can find how to do some common database tasks using SQLAlchemy and SQLA-wrapper. Is not a complete reference of what you can do with SQLAlchemy so you should read the official [SQLAlchemy tutorial](https://docs.sqlalchemy.org/en/14/tutorial/) to have a better understanding of it.
In this section you can find how to do some common database tasks using SQLAlchemy and SQLA-wrapper. Is not a complete reference of what you can do with SQLAlchemy so you should read the official [SQLAlchemy tutorial](https://docs.sqlalchemy.org/en/20/tutorial/) to have a better understanding of it.

All examples assume that an SQLAlchemy instance has been created and stored in a global variable named `db`.


## Declare models

`db` provides a `db.Model` class to be used as a declarative base class for your models.
`db` provides a `db.Model` class to be used as a declarative base class for your models and follow the new [type-based way to declare the table columns](https://docs.sqlalchemy.org/en/20/tutorial/metadata.html#declaring-mapped-classes)

```python
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from myapp.models import db

class User(db.Model):
id = Column(Integer, primary_key=True)
name = Column(String(128))
```

`db` also includes all the functions and classes from `sqlalchemy` and `sqlalchemy.orm` so you don't need to import `Column`, `Integer`, `String`, etc. and can do this instead:
__tablename__ = "users"

```python
from myapp.models import db

class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128))
```

To learn more about how to define database models, consult the [SQLAlchemy ORM documentation](https://docs.sqlalchemy.org/en/14/orm/index.html).


## Insert an object to the database

Expand Down Expand Up @@ -81,24 +71,25 @@ user = db.s.first(User, login="hello")

## Query the database

First, make a query using `db.select( ... )`, and then execute the query with `db.s.execute( ... )`.
First, make a query using `sqlalchemy.select( ... )`, and then execute the query with `db.s.execute( ... )`.

```python
import sqlalchemy as sa
from myapp.models import User, db

users = db.s.execute(
db.select(User)
sa.select(User)
.where(User.email.endswith('@example.com'))
).scalars()

# You can now do `users.all()`, `users.first()`,
# `users.unique()`, etc.
```

The [results](https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.Result) from `db.s.execute()` are returned as a list of rows, where each row is a tuple, even if only one result per row was requested. The [`scalars()`](https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.ScalarResult) method conveniently extract the first result in each row.
The [results](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.Result) from `db.s.execute()` are returned as a list of rows, where each row is a tuple, even if only one result per row was requested. The [`scalars()`](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.ScalarResult) method conveniently extract the first result in each row.

The `select()` function it is very powerful and can do **a lot** more:
https://docs.sqlalchemy.org/en/14/tutorial/data_select.html#selecting-rows-with-core-or-orm
https://docs.sqlalchemy.org/en/20/tutorial/data_select.html#selecting-rows-with-core-or-orm



Expand All @@ -107,10 +98,11 @@ https://docs.sqlalchemy.org/en/14/tutorial/data_select.html#selecting-rows-with-
Like with regular SQL, use the `count` function:

```python
import sqlalchemy as sa
from myapp.models import User, db

num = db.s.execute(
db.select(db,func.count(User.id))
sa.select(db,func.count(User.id))
.where(User.email.endswith('@example.com'))
).scalar()
```
Expand Down Expand Up @@ -147,18 +139,19 @@ db.s.commit()

## Run an arbitrary SQL statement

Use `db.text` to build a query and then run it with `db.s.execute`.
Use `sqlalchemy.text` to build a query and then run it with `db.s.execute`.

```python
import sqlalchemy as sa
from myapp.models import db

sql = db.text("SELECT * FROM user WHERE user.id = :user_id")
sql = sa.text("SELECT * FROM user WHERE user.id = :user_id")
results = db.s.execute(sql, params={"user_id": 5}).all()
```

Parameters are specified by name, always using the format `:name`, no matter the database engine.

Is important to use `db.text()` instead of plain strings so the parameters are escaped protecting you from SQL injection attacks.
Is important to use `text()` instead of plain strings so the parameters are escaped protecting you from SQL injection attacks.


## Work with background jobs/tasks
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ template: home.html

SQLA-Wrapper is a wrapper for SQLAlchemy and Alembic that simplifies many aspects of its setup.

It works with the [newer 2.0 style query API introduced in SQLAlchemy 1.4](https://docs.sqlalchemy.org/en/14/glossary.html#term-2.0-style), and can be used with most web frameworks.
It works with the [newer SQLAlchemy 2.0 style](https://docs.sqlalchemy.org/en/20/glossary.html#term-2.0-style), and can be used with most web frameworks.


## Includes

- A [SQLAlchemy wrapper](sqlalchemy-wrapper), that does all the SQLAlchemy setup and gives you:
- A scoped session extended with some useful active-record-like methods and pagination.
- A scoped session extended with some useful active-record-like methods.
- A declarative base class.
- A helper for performant testing with a real database.

Expand Down
25 changes: 7 additions & 18 deletions docs/docs/sqlalchemy-wrapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,37 +35,26 @@ db = SQLAlchemy(
)
```

After the setup, you will be interacting mostly directly with SQLAlchemy so I recommend reading the official [SQLAlchemy tutorial](https://docs.sqlalchemy.org/en/14/tutorial/index.html) if you haven't done it yet.
After the setup, you will be interacting mostly directly with SQLAlchemy so I recommend reading the official [SQLAlchemy tutorial](https://docs.sqlalchemy.org/en/20/tutorial/index.html) if you haven't done it yet.

Beyond the URI, the class also accepts an `engine_options` and a `session_options` dictionary to pass special options when creating the engine and/or the session.


## Declaring models

A `SQLAlchemy` instance provides a `db.Model` class to be used as a declarative base class for your models.
A `SQLAlchemy` instance provides a `db.Model` class to be used as a declarative base class for your models. Follow the new [type-based way to declare the table columns](https://docs.sqlalchemy.org/en/20/tutorial/metadata.html#declaring-mapped-classes)

```python
from sqlalchemy import Column, Integer, String
from .base import db
from sqlalchemy.orm import Mapped, mapped_column
from myapp.models import db

class User(db.Model):
id = Column(Integer, primary_key=True)
name = Column(String(128))
```

`db` also includes all the functions and classes from `sqlalchemy` and `sqlalchemy.orm` so you don't need to import `Column`, `Integer`, `String`, etc. and can do this instead:

```python
from .base import db
__tablename__ = "users"

class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(128))
```

To learn more about how to define database models, consult the [SQLAlchemy ORM documentation](https://docs.sqlalchemy.org/en/14/orm/index.html).


## API

::: sqla_wrapper.SQLAlchemy
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/testing-with-a-real-database.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ There is only one caveat: if you want to allow tests to also use rollbacks withi
connection.close()
```

This recipe is what [SQLAlchemy documentation](https://docs.sqlalchemy.org/en/14/orm/session_transaction.html#session-external-transaction) recommends and even test in their own CI to ensure that it remains working as expected.
This recipe is what [SQLAlchemy documentation](https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#session-external-transaction) recommends and even test in their own CI to ensure that it remains working as expected.


### pytest
Expand Down
29 changes: 22 additions & 7 deletions docs/docs/working-with-the-session.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
The Session is the mean to communicate with the database.
There are two main ways to use it:


## Use the scoped session `db.s`
## In a web app: use the scoped session `db.s`

The "scoped_session" is really a proxy to a session automatically scoped to the current thread.

Expand All @@ -22,10 +21,26 @@ def remove_db_scoped_session(error=None):

The `db.s.remove()` method close the current session and dispose it. A new session will be created when `db.s` is called again.

Outside a web request cycle, like in a background job, you must call manually call `db.s.remove()` at the end.
## In a web app background job

Outside a web request cycle, like in a background job, you still can use the global session, but you must:

1. Call `db.engine.dispose()` when each new process is created.
2. Call `db.s.remove()` at the end of each job/task

Background jobs libraries, like Celery or RQ, use multiprocessing or `fork()`, to have several "workers" to run these jobs. When that happens, the pool of connections to the database is copied to the child processes, which does causes errors.

For that reason you should call `db.engine.dispose()` when each worker process is created, so that the engine creates brand new database connections local to that fork.

You also must remember to call `db.s.remove()` at the end of each job, so a new session is used each time.

With most background jobs libraries you can set them so it's done automatically, see:

- [Working with RQ](how-to/#rq)
- [Working with Celery](how-to/#celery)


## Instantiate `db.Session`
## In a standalone script: Instantiate `db.Session`

You can use a context manager:

Expand Down Expand Up @@ -55,11 +70,11 @@ SQLAlchemy default Session class has the method `.get(Model, pk)`
to query and return a record by its primary key.

This class extends the `sqlalchemy.orm.Session` class with some useful
active-record-like methods and a pagination helper.
active-record-like methods.

::: sqla_wrapper.Session
:members: all create first first_or_create create_or_first paginate
:members: all create first first_or_create create_or_first

---

As always, I recommend reading the official [SQLAlchemy tutorial](https://docs.sqlalchemy.org/en/14/tutorial/orm_data_manipulation.html#tutorial-orm-data-manipulation) to learn more how to work with the session.
As always, I recommend reading the official [SQLAlchemy tutorial](https://docs.sqlalchemy.org/en/20/tutorial/orm_data_manipulation.html#tutorial-orm-data-manipulation) to learn more how to work with the session.
2 changes: 1 addition & 1 deletion docs/overrides/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
<h1>SQLA-Wrapper</h1>
<h2>
A friendly wrapper for
<a href="https://docs.sqlalchemy.org/en/14/glossary.html#term-2.0-style"
<a href="https://docs.sqlalchemy.org/en/20/glossary.html#term-2.0-style"
>modern SQLAlchemy</a
>
and&nbsp;<a href="https://alembic.sqlalchemy.org/en/latest/">Alembic.</a>
Expand Down
22 changes: 10 additions & 12 deletions src/sqla_wrapper/alembic_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from __future__ import annotations

import shutil
import typing as t
from pathlib import Path
Expand Down Expand Up @@ -53,7 +51,7 @@ def __init__(

def revision(
self, message: str, *, empty: bool = False, parent: str = "head"
) -> Script | None:
) -> "Script | None":
"""Create a new revision.
Auto-generate operations by comparing models and database.
Expand Down Expand Up @@ -172,7 +170,7 @@ def do_downgrade(revision, context):
)

def get_history(
self, *, start: str | None = None, end: str | None = None
self, *, start: "str | None" = None, end: "str | None" = None
) -> list[Script]:
"""Get the list of revisions in chronological order.
You can optionally specify the range of revisions to return.
Expand Down Expand Up @@ -200,8 +198,8 @@ def history(
self,
*,
verbose: bool = False,
start: str | None = "base",
end: str | None = "heads",
start: "str | None" = "base",
end: "str | None" = "heads",
) -> None:
"""Print the list of revisions in chronological order.
You can optionally specify the range of revisions to return.
Expand Down Expand Up @@ -255,7 +253,7 @@ def do_stamp(revision, context):
purge=purge,
)

def _get_currents(self) -> t.Tuple[Script | None, ...]:
def _get_currents(self) -> "t.Tuple[Script | None, ...]":
"""Get the last revisions applied."""
env = EnvironmentContext(self.config, self.script_directory)
with self.db.engine.connect() as connection:
Expand All @@ -265,7 +263,7 @@ def _get_currents(self) -> t.Tuple[Script | None, ...]:

return self.script_directory.get_revisions(current_heads)

def get_current(self) -> Script | None:
def get_current(self) -> "Script | None":
"""Get the last revision applied."""
revisions = self._get_currents()
return revisions[0] if revisions else None
Expand All @@ -290,11 +288,11 @@ def current(self, verbose: bool = False) -> None:
)
)

def _get_heads(self) -> t.Tuple[Script | None, ...]:
def _get_heads(self) -> "t.Tuple[Script | None, ...]":
"""Get the list of the latest revisions."""
return self.script_directory.get_revisions("heads")

def get_head(self) -> Script | None:
def get_head(self) -> "Script | None":
"""Get the latest revision."""
heads = self._get_heads()
return heads[0] if heads else None
Expand Down Expand Up @@ -376,7 +374,7 @@ def _get_config(self, options: dict[str, str]) -> Config:
return config

def _run_online(
self, fn: t.Callable, *, kwargs: dict | None = None, **envargs
self, fn: t.Callable, *, kwargs: "dict | None" = None, **envargs
) -> None:
"""Emit the SQL to the database."""
env = EnvironmentContext(self.config, self.script_directory)
Expand All @@ -392,7 +390,7 @@ def _run_online(
env.run_migrations(**kwargs)

def _run_offline(
self, fn: t.Callable, *, kwargs: dict | None = None, **envargs
self, fn: t.Callable, *, kwargs: "dict | None" = None, **envargs
) -> None:
"""Don't emit SQL to the database, dump to standard output instead."""
env = EnvironmentContext(self.config, self.script_directory)
Expand Down
2 changes: 1 addition & 1 deletion src/sqla_wrapper/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Session(sqlalchemy.orm.Session):
to query and return a record by its primary key.
This class extends the `sqlalchemy.orm.Session` class with some useful
active-record-like methods and a pagination helper.
active-record-like methods.
"""

def all(self, Model: t.Any, **attrs) -> t.Sequence[t.Any]:
Expand Down

0 comments on commit ce09ad4

Please sign in to comment.