Skip to content

Commit 4d8705f

Browse files
committed
Switches JSON type from TEXT to JSONB.
1 parent 0d0976a commit 4d8705f

File tree

6 files changed

+36
-27
lines changed

6 files changed

+36
-27
lines changed

HISTORY.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
Changelog
22
---------
33

4+
- Replaces `JSON` database type with `JSONB`, this means
5+
Postgres as a backend is not required. You will also need
6+
to write a migration for existing JSON columns. You may use
7+
the following recipe using an alembic `Operations` object::
8+
9+
operations.alter_column(
10+
'table_name',
11+
'column_name',
12+
type_=JSON,
13+
postgresql_using='"column_name"::jsonb'
14+
)
15+
416
0.8.0 (15.01.2025)
517
~~~~~~~~~~~~~~~~~~~
618

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ Libres
22
======
33

44
Libres is a reservations management library to reserve things like tables at
5-
a restaurant or tickets at an event. It works with Python 3.8+
6-
and requires Postgresql 9.1+.
5+
a restaurant or tickets at an event. It works with Python 3.9+
6+
and requires Postgresql 9.2+.
77

88
`Documentation <http://libres.readthedocs.org/en/latest/>`_ | `Source <http://github.com/seantis/libres/>`_ | `Bugs <http://github.com/seantis/libres/issues>`_
99

src/libres/db/models/allocation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
118118
timezone: Column[str | None] = Column(types.String())
119119

120120
#: Custom data reserved for the user
121-
data: Column[Any | None] = Column(
121+
data: Column[dict[str, Any] | None] = Column(
122122
JSON(),
123123
nullable=True
124124
)

src/libres/db/models/reservation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
9696
nullable=False
9797
)
9898

99-
data: Column[Any | None] = deferred(
99+
data: Column[dict[str, Any] | None] = deferred(
100100
Column(
101101
JSON(),
102102
nullable=True
Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,46 @@
11
from __future__ import annotations
22

3-
from json import loads, dumps
4-
from sqlalchemy.types import TypeDecorator, TEXT
3+
from sqlalchemy.ext.mutable import MutableDict
4+
from sqlalchemy.types import TypeDecorator
5+
from sqlalchemy.dialects.postgresql import JSONB
56

67

78
from typing import Any
89
from typing import TYPE_CHECKING
910
if TYPE_CHECKING:
1011
from sqlalchemy.engine import Dialect
1112

12-
_Base = TypeDecorator[Any]
13+
_Base = TypeDecorator[dict[str, Any]]
1314
else:
1415
_Base = TypeDecorator
1516

1617

1718
class JSON(_Base):
18-
"""Like the default JSON, but using the json serializer from the dialect
19-
(postgres) each time the value is read, even if it never left the ORM. The
20-
default json type will only do it when the record is read from the
21-
database.
19+
""" A JSONB based type that coerces None's to empty dictionaries.
20+
21+
That is, this JSONB column cannot be `'null'::jsonb`. It could
22+
still be `NULL` though, if it's nullable and never explicitly
23+
set. But on the Python end you should always see a dictionary.
2224
2325
"""
2426

25-
# FIXME: We should switch to JSONB now, but we will need to
26-
# add a database migration to OneGov at the same time
27-
impl = TEXT
27+
impl = JSONB
2828

29-
def process_bind_param(
29+
def process_bind_param( # type:ignore[override]
3030
self,
31-
value: Any,
31+
value: dict[str, Any] | None,
3232
dialect: Dialect
33-
) -> str | None:
34-
35-
if value is not None:
36-
value = (dialect._json_serializer or dumps)(value) # type:ignore
33+
) -> dict[str, Any]:
3734

38-
return value # type: ignore[no-any-return]
35+
return {} if value is None else value
3936

4037
def process_result_value(
4138
self,
42-
value: str | None,
39+
value: dict[str, Any] | None,
4340
dialect: Dialect
44-
) -> Any | None:
41+
) -> dict[str, Any]:
42+
43+
return {} if value is None else value
4544

46-
if value is not None:
47-
value = (dialect._json_deserializer or loads)(value) # type:ignore
4845

49-
return value
46+
MutableDict.associate_with(JSON) # type:ignore[no-untyped-call]

tests/test_scheduler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1821,7 +1821,7 @@ def session_provider(context):
18211821
scheduler.allocate((start, end), data=None)
18221822
scheduler.commit()
18231823

1824-
assert scheduler.managed_allocations().first().data is None
1824+
assert scheduler.managed_allocations().first().data == {}
18251825

18261826

18271827
def test_no_reservations_to_confirm(scheduler):

0 commit comments

Comments
 (0)