Skip to content

Commit

Permalink
intervals support (closes #30)
Browse files Browse the repository at this point in the history
  • Loading branch information
igorcoding committed Jan 2, 2024
1 parent 2277115 commit 0368328
Show file tree
Hide file tree
Showing 25 changed files with 823 additions and 248 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## v2.2.0
**New features:**
* Added support for [interval types](https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/interval_object/) [#30](https://github.com/igorcoding/asynctnt/issues/30)

## v2.1.0
**Breaking changes:**
* Dropped support for Python 3.6
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ Documentation is available [here](https://igorcoding.github.io/asynctnt).
* Full support for [SQL](https://www.tarantool.io/en/doc/latest/tutorials/sql_tutorial/),
including [prepared statements](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_sql/prepare/).
* Support for [interactive transaction](https://www.tarantool.io/en/doc/latest/book/box/atomic/txn_mode_mvcc/) via Tarantool streams.
* Support of `Decimal`, `UUID` and `datetime` types natively.
* Support of `Decimal`, `UUID`,`datetime` types natively.
* Support for [interval types](https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/interval_object/).
* Support for parsing [custom errors](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/new/).
* **Schema fetching** on connection establishment, so you can use spaces and
indexes names rather than their ids, and **auto refetching** if schema in
Expand Down
2 changes: 2 additions & 0 deletions asynctnt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

from .connection import Connection, connect
from .iproto.protocol import (
Adjust,
Db,
Field,
IProtoError,
IProtoErrorStackFrame,
Iterator,
Metadata,
MPInterval,
PushIterator,
Response,
Schema,
Expand Down
1 change: 1 addition & 0 deletions asynctnt/iproto/buffer.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ cdef class WriteBuffer:
cdef char *mp_encode_decimal(self, char *p, object value) except NULL
cdef char *mp_encode_uuid(self, char *p, object value) except NULL
cdef char *mp_encode_datetime(self, char *p, object value) except NULL
cdef char *mp_encode_interval(self, char *p, MPInterval value) except NULL
cdef char *mp_encode_array(self, char *p, uint32_t len) except NULL
cdef char *mp_encode_map(self, char *p, uint32_t len) except NULL
cdef char *mp_encode_list(self, char *p, list arr) except NULL
Expand Down
17 changes: 17 additions & 0 deletions asynctnt/iproto/buffer.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,20 @@ cdef class WriteBuffer:
self._length += (p - begin)
return p

cdef char *mp_encode_interval(self, char *p, MPInterval value) except NULL:
cdef:
char *begin
char *data_p
uint32_t length

length = interval_len(value)
p = begin = self._ensure_allocated(p, mp_sizeof_ext(length))
p = mp_encode_extl(p, tarantool.MP_INTERVAL, length)
p = interval_encode(p, value)

self._length += (p - begin)
return p

cdef char *mp_encode_array(self, char *p, uint32_t len) except NULL:
cdef char *begin
p = begin = self._ensure_allocated(p, mp_sizeof_array(len))
Expand Down Expand Up @@ -406,6 +420,9 @@ cdef class WriteBuffer:
elif isinstance(o, datetime):
return self.mp_encode_datetime(p, o)

elif isinstance(o, MPInterval):
return self.mp_encode_interval(p, <MPInterval> o)

elif isinstance(o, Decimal):
return self.mp_encode_decimal(p, o)

Expand Down
4 changes: 4 additions & 0 deletions asynctnt/iproto/cmsgpuck.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ cdef extern from "../../third_party/msgpuck/msgpuck.h":
cdef char *mp_store_u32(char *data, uint32_t val)
cdef char *mp_store_u64(char *data, uint64_t val)

cdef ptrdiff_t mp_check_uint(const char *cur, const char *end)
cdef ptrdiff_t mp_check_int(const char *cur, const char *end)

cdef mp_type mp_typeof(const char c)

cdef uint32_t mp_sizeof_array(uint32_t size)
Expand All @@ -43,6 +46,7 @@ cdef extern from "../../third_party/msgpuck/msgpuck.h":
cdef uint32_t mp_sizeof_int(int64_t num)
cdef char *mp_encode_int(char *data, int64_t num)
cdef int64_t mp_decode_int(const char **data)
cdef int mp_read_int64(const char **data, int64_t *ret)

cdef uint32_t mp_sizeof_float(float num)
cdef char *mp_encode_float(char *data, float num)
Expand Down
32 changes: 0 additions & 32 deletions asynctnt/iproto/ext.pxd

This file was deleted.

18 changes: 18 additions & 0 deletions asynctnt/iproto/ext/datetime.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from cpython.datetime cimport datetime
from libc.stdint cimport int16_t, int32_t, int64_t, uint32_t


cdef struct IProtoDateTime:
int64_t seconds
int32_t nsec
int16_t tzoffset
int16_t tzindex

cdef void datetime_zero(IProtoDateTime *dt)
cdef uint32_t datetime_len(IProtoDateTime *dt)
cdef char *datetime_encode(char *p, IProtoDateTime *dt) except NULL
cdef int datetime_decode(const char ** p,
uint32_t length,
IProtoDateTime *dt) except -1
cdef void datetime_from_py(datetime ob, IProtoDateTime *dt)
cdef object datetime_to_py(IProtoDateTime *dt)
87 changes: 87 additions & 0 deletions asynctnt/iproto/ext/datetime.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
cimport cpython.datetime
from cpython.datetime cimport PyDateTimeAPI, datetime, datetime_tzinfo, timedelta_new
from libc.stdint cimport uint32_t
from libc.string cimport memcpy


cdef inline void datetime_zero(IProtoDateTime *dt):
dt.seconds = 0
dt.nsec = 0
dt.tzoffset = 0
dt.tzindex = 0

cdef inline uint32_t datetime_len(IProtoDateTime *dt):
cdef uint32_t sz
sz = sizeof(int64_t)
if dt.nsec != 0 or dt.tzoffset != 0 or dt.tzindex != 0:
return sz + DATETIME_TAIL_SZ
return sz

cdef char *datetime_encode(char *p, IProtoDateTime *dt) except NULL:
store_u64(p, dt.seconds)
p += sizeof(dt.seconds)
if dt.nsec != 0 or dt.tzoffset != 0 or dt.tzindex != 0:
memcpy(p, &dt.nsec, DATETIME_TAIL_SZ)
p += DATETIME_TAIL_SZ
return p

cdef int datetime_decode(
const char ** p,
uint32_t length,
IProtoDateTime *dt
) except -1:
delta = None
tz = None

dt.seconds = load_u64(p[0])
p[0] += sizeof(dt.seconds)
length -= sizeof(dt.seconds)

if length == 0:
return 0

if length != DATETIME_TAIL_SZ:
raise ValueError("invalid datetime size. got {} extra bytes".format(
length
))

dt.nsec = load_u32(p[0])
p[0] += 4
dt.tzoffset = load_u16(p[0])
p[0] += 2
dt.tzindex = load_u16(p[0])
p[0] += 2

cdef void datetime_from_py(datetime ob, IProtoDateTime *dt):
cdef:
double ts
int offset
ts = <double> ob.timestamp()
dt.seconds = <int64_t> ts
dt.nsec = <int32_t> ((ts - <double> dt.seconds) * 1000000) * 1000
if dt.nsec < 0:
# correction for negative dates
dt.seconds -= 1
dt.nsec += 1000000000

if datetime_tzinfo(ob) is not None:
offset = ob.utcoffset().total_seconds()
dt.tzoffset = <int16_t> (offset / 60)

cdef object datetime_to_py(IProtoDateTime *dt):
cdef:
double timestamp
object tz

tz = None

if dt.tzoffset != 0:
delta = timedelta_new(0, <int> dt.tzoffset * 60, 0)
tz = timezone_new(delta)

timestamp = dt.seconds + (<double> dt.nsec) / 1e9
return PyDateTimeAPI.DateTime_FromTimestamp(
<PyObject *>PyDateTimeAPI.DateTimeType,
(timestamp,) if tz is None else (timestamp, tz),
NULL,
)
14 changes: 14 additions & 0 deletions asynctnt/iproto/ext/decimal.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from libc cimport math
from libc.stdint cimport uint8_t, uint32_t


cdef inline uint32_t bcd_len(uint32_t digits_len):
return <uint32_t> math.floor(digits_len / 2) + 1

cdef uint32_t decimal_len(int exponent, uint32_t digits_count)
cdef char *decimal_encode(char *p,
uint32_t digits_count,
uint8_t sign,
tuple digits,
int exponent) except NULL
cdef object decimal_decode(const char ** p, uint32_t length)
91 changes: 0 additions & 91 deletions asynctnt/iproto/ext.pyx → asynctnt/iproto/ext/decimal.pyx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
cimport cpython.datetime
from cpython.datetime cimport PyDateTimeAPI, datetime, datetime_tzinfo, timedelta_new
from libc.stdint cimport uint32_t
from libc.string cimport memcpy

from decimal import Decimal
from uuid import UUID


cdef uint32_t decimal_len(int exponent, uint32_t digits_count):
Expand Down Expand Up @@ -127,90 +123,3 @@ cdef object decimal_decode(const char ** p, uint32_t length):
p[0] += length

return Decimal((<object> <int> sign, digits, <object> exponent))

cdef object uuid_decode(const char ** p, uint32_t length):
data = cpython.bytes.PyBytes_FromStringAndSize(p[0], length)
p[0] += length
return UUID(bytes=data)

cdef inline void datetime_zero(IProtoDateTime *dt):
dt.seconds = 0
dt.nsec = 0
dt.tzoffset = 0
dt.tzindex = 0

cdef inline uint32_t datetime_len(IProtoDateTime *dt):
cdef uint32_t sz
sz = sizeof(int64_t)
if dt.nsec != 0 or dt.tzoffset != 0 or dt.tzindex != 0:
return sz + DATETIME_TAIL_SZ
return sz

cdef char *datetime_encode(char *p, IProtoDateTime *dt) except NULL:
store_u64(p, dt.seconds)
p += sizeof(dt.seconds)
if dt.nsec != 0 or dt.tzoffset != 0 or dt.tzindex != 0:
memcpy(p, &dt.nsec, DATETIME_TAIL_SZ)
p += DATETIME_TAIL_SZ
return p

cdef int datetime_decode(
const char ** p,
uint32_t length,
IProtoDateTime *dt
) except -1:
delta = None
tz = None

dt.seconds = load_u64(p[0])
p[0] += sizeof(dt.seconds)
length -= sizeof(dt.seconds)

if length == 0:
return 0

if length != DATETIME_TAIL_SZ:
raise ValueError("invalid datetime size. got {} extra bytes".format(
length
))

dt.nsec = load_u32(p[0])
p[0] += 4
dt.tzoffset = load_u16(p[0])
p[0] += 2
dt.tzindex = load_u16(p[0])
p[0] += 2

cdef void datetime_from_py(datetime ob, IProtoDateTime *dt):
cdef:
double ts
int offset
ts = <double> ob.timestamp()
dt.seconds = <int64_t> ts
dt.nsec = <int32_t> ((ts - <double> dt.seconds) * 1000000) * 1000
if dt.nsec < 0:
# correction for negative dates
dt.seconds -= 1
dt.nsec += 1000000000

if datetime_tzinfo(ob) is not None:
offset = ob.utcoffset().total_seconds()
dt.tzoffset = <int16_t> (offset / 60)

cdef object datetime_to_py(IProtoDateTime *dt):
cdef:
double timestamp
object tz

tz = None

if dt.tzoffset != 0:
delta = timedelta_new(0, <int> dt.tzoffset * 60, 0)
tz = timezone_new(delta)

timestamp = dt.seconds + (<double> dt.nsec) / 1e9
return PyDateTimeAPI.DateTime_FromTimestamp(
<PyObject *>PyDateTimeAPI.DateTimeType,
(timestamp,) if tz is None else (timestamp, tz),
NULL,
)
15 changes: 15 additions & 0 deletions asynctnt/iproto/ext/error.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
cdef class IProtoErrorStackFrame:
cdef:
readonly str error_type
readonly str file
readonly int line
readonly str message
readonly int err_no
readonly int code
readonly dict fields

cdef class IProtoError:
cdef:
readonly list trace

cdef IProtoError iproto_error_decode(const char ** b, bytes encoding)
Loading

0 comments on commit 0368328

Please sign in to comment.