Skip to content

Commit eebf552

Browse files
committed
added Numeric and Real column types
1 parent b3ad082 commit eebf552

File tree

4 files changed

+153
-0
lines changed

4 files changed

+153
-0
lines changed

piccolo/columns/column_types.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22
import copy
33
from datetime import datetime
4+
import decimal
45
import typing as t
56
import uuid
67

@@ -231,6 +232,78 @@ def __init__(self, default: bool = False, **kwargs) -> None:
231232
super().__init__(**kwargs)
232233

233234

235+
###############################################################################
236+
237+
238+
class Numeric(Column):
239+
"""
240+
Used to represent values precisely. The value is returned as a Decimal.
241+
"""
242+
243+
value_type = decimal.Decimal
244+
245+
@property
246+
def column_type(self):
247+
if self.precision and self.scale:
248+
return f"NUMERIC({self.precision}, {self.scale})"
249+
else:
250+
return "NUMERIC"
251+
252+
def __init__(
253+
self,
254+
precision: t.Optional[int] = None,
255+
scale: t.Optional[int] = None,
256+
default: decimal.Decimal = decimal.Decimal(0.0),
257+
**kwargs,
258+
) -> None:
259+
if (precision, scale).count(None) == 1:
260+
raise ValueError(
261+
"The precision and scale args should either both be None, or "
262+
"neither be None."
263+
)
264+
265+
self.default = default
266+
self.precision = precision
267+
self.scale = scale
268+
kwargs.update(
269+
{"default": default, "precision": precision, "scale": scale}
270+
)
271+
super().__init__(**kwargs)
272+
273+
274+
class Decimal(Numeric):
275+
"""
276+
An alias for Numeric.
277+
"""
278+
279+
pass
280+
281+
282+
class Real(Column):
283+
"""
284+
Can be used instead of Numeric when precision isn't as important. The value
285+
is returned as a float.
286+
"""
287+
288+
value_type = float
289+
290+
def __init__(self, default: float = 0.0, **kwargs) -> None:
291+
self.default = default
292+
kwargs.update({"default": default})
293+
super().__init__(**kwargs)
294+
295+
296+
class Float(Real):
297+
"""
298+
An alias for Real.
299+
"""
300+
301+
pass
302+
303+
304+
###############################################################################
305+
306+
234307
class ForeignKey(Integer):
235308
"""
236309
Returns an integer, representing the referenced row's ID.

piccolo/engine/sqlite.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22
import contextvars
33
from dataclasses import dataclass
4+
from decimal import Decimal
45
import os
56
import sqlite3
67
import typing as t
@@ -14,6 +15,31 @@
1415
from piccolo.querystring import QueryString
1516
from piccolo.utils.sync import run_sync
1617

18+
###############################################################################
19+
20+
# We need to register some adapters so sqlite returns types which are more
21+
# consistent with the Postgres engine.
22+
23+
24+
def convert_numeric_out(value: bytes):
25+
"""
26+
Converts the value coming from sqlite.
27+
"""
28+
return Decimal(value.decode("ascii"))
29+
30+
31+
def convert_numeric_in(value):
32+
"""
33+
Converts the value being passed into sqlite.
34+
"""
35+
return value if isinstance(value, float) else float(value)
36+
37+
38+
sqlite3.register_converter("Numeric", convert_numeric_out)
39+
sqlite3.register_adapter(Decimal, convert_numeric_in)
40+
41+
###############################################################################
42+
1743

1844
@dataclass
1945
class AsyncBatch(Batch):

tests/columns/test_numeric.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from decimal import Decimal
2+
from unittest import TestCase
3+
4+
from piccolo.table import Table
5+
from piccolo.columns.column_types import Numeric
6+
7+
8+
class MyTable(Table):
9+
column_a = Numeric()
10+
column_b = Numeric(precision=3, scale=2)
11+
12+
13+
class TestNumeric(TestCase):
14+
def setUp(self):
15+
MyTable.create_table().run_sync()
16+
17+
def tearDown(self):
18+
MyTable.alter().drop_table().run_sync()
19+
20+
def test_creation(self):
21+
row = MyTable(column_a=Decimal(1.23), column_b=Decimal(1.23))
22+
row.save().run_sync()
23+
24+
_row = MyTable.objects().first().run_sync()
25+
26+
self.assertTrue(type(_row.column_a) == Decimal)
27+
self.assertTrue(type(_row.column_b) == Decimal)
28+
29+
self.assertAlmostEqual(_row.column_a, Decimal(1.23))
30+
self.assertEqual(_row.column_b, Decimal("1.23"))

tests/columns/test_real.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from unittest import TestCase
2+
3+
from piccolo.table import Table
4+
from piccolo.columns.column_types import Real
5+
6+
7+
class MyTable(Table):
8+
column_a = Real()
9+
10+
11+
class TestReal(TestCase):
12+
def setUp(self):
13+
MyTable.create_table().run_sync()
14+
15+
def tearDown(self):
16+
MyTable.alter().drop_table().run_sync()
17+
18+
def test_creation(self):
19+
row = MyTable(column_a=1.23)
20+
row.save().run_sync()
21+
22+
_row = MyTable.objects().first().run_sync()
23+
self.assertTrue(type(_row.column_a) == float)
24+
self.assertAlmostEqual(_row.column_a, 1.23)

0 commit comments

Comments
 (0)