Skip to content

Commit f557b9e

Browse files
committed
Add upsert
Modify docstrings for tarantool.Connection methods Modify docstrings for tarantool.Space methods Add basic tests for upsert/eval. Modify tarantool.Space for simplicity of code/understanding Add tests for space, fix couple of errors in code
1 parent b90eef6 commit f557b9e

File tree

5 files changed

+233
-78
lines changed

5 files changed

+233
-78
lines changed

tarantool/connection.py

Lines changed: 131 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
RequestSelect,
3333
RequestSubscribe,
3434
RequestUpdate,
35+
RequestUpsert,
3536
RequestAuthenticate)
3637

3738
from tarantool.space import Space
@@ -331,7 +332,7 @@ def eval(self, expr, *args):
331332
def replace(self, space_name, values):
332333
'''
333334
Execute REPLACE request.
334-
It will throw error if there's no tuple with this PK exists
335+
It won't throw error if there's no tuple with this PK exists
335336
336337
:param int space_name: space id to insert a record
337338
:type space_name: int or str
@@ -347,6 +348,14 @@ def replace(self, space_name, values):
347348
return self._send_request(request)
348349

349350
def authenticate(self, user, password):
351+
'''
352+
Execute AUTHENTICATE request.
353+
354+
:param string user: user to authenticate with
355+
:param string password: password for the user
356+
357+
:rtype: `Response` instance
358+
'''
350359
self.user = user
351360
self.password = password
352361
if not self._socket:
@@ -408,7 +417,8 @@ def insert(self, space_name, values):
408417
def delete(self, space_name, key, **kwargs):
409418
'''
410419
Execute DELETE request.
411-
Delete single record identified by `key` (using primary index).
420+
Delete single record identified by `key`. If you're using secondary
421+
index, it must be unique.
412422
413423
:param space_name: space number or name to delete a record
414424
:type space_name: int or name
@@ -427,25 +437,139 @@ def delete(self, space_name, key, **kwargs):
427437
request = RequestDelete(self, space_name, index_name, key)
428438
return self._send_request(request)
429439

440+
def upsert(self, space_name, tuple_value, op_list, **kwargs):
441+
'''
442+
Execute UPSERT request.
443+
444+
If there is an existing tuple which matches the key fields of
445+
`tuple_value`, then the request has the same effect as UPDATE
446+
and the [(field_1, symbol_1, arg_1), ...] parameter is used.
447+
448+
If there is no existing tuple which matches the key fields of
449+
`tuple_value`, then the request has the same effect as INSERT
450+
and the `tuple_value` parameter is used. However, unlike insert
451+
or update, upsert will not read a tuple and perform error checks
452+
before returning -- this is a design feature which enhances
453+
throughput but requires more caution on the part of the user.
454+
455+
If you're using secondary index, it must be unique.
456+
457+
List of operations allows to update individual fields.
458+
459+
*Allowed operations:*
460+
461+
(For every operation you must provide field number, to apply this
462+
operation to)
463+
464+
* `+` for addition (values must be numeric)
465+
* `-` for subtraction (values must be numeric)
466+
* `&` for bitwise AND (values must be unsigned numeric)
467+
* `|` for bitwise OR (values must be unsigned numeric)
468+
* `^` for bitwise XOR (values must be unsigned numeric)
469+
* `:` for string splice (you must provide `offset`, `count` and `value`
470+
for this operation)
471+
* `!` for insertion (provide any element to insert)
472+
* `=` for assignment (provide any element to assign)
473+
* `#` for deletion (provide count of fields to delete)
474+
475+
:param space_name: space number or name to update a record
476+
:type space_name: int or str
477+
:param index: index number or name to update a record
478+
:type index: int or str
479+
:param tuple_value: tuple, that
480+
:type tuple_value:
481+
:param op_list: list of operations. Each operation
482+
is tuple of three (or more) values
483+
:type op_list: a list of the form [(symbol_1, field_1, arg_1),
484+
(symbol_2, field_2, arg_2_1, arg_2_2, arg_2_3),...]
485+
486+
:rtype: `Response` instance
487+
488+
Operation examples:
489+
490+
.. code-block:: python
491+
492+
# 'ADD' 55 to second field
493+
# Assign 'x' to third field
494+
[('+', 2, 55), ('=', 3, 'x')]
495+
# 'OR' third field with '1'
496+
# Cut three symbols starting from second and replace them with '!!'
497+
# Insert 'hello, world' field before fifth element of tuple
498+
[('|', 3, 1), (':', 2, 2, 3, '!!'), ('!', 5, 'hello, world')]
499+
# Delete two fields starting with second field
500+
[('#', 2, 2)]
501+
'''
502+
index_name = kwargs.get("index", 0)
503+
504+
if isinstance(space_name, six.string_types):
505+
space_name = self.schema.get_space(space_name).sid
506+
if isinstance(index_name, six.string_types):
507+
index_name = self.schema.get_index(space_name, index_name).iid
508+
request = RequestUpsert(self, space_name, index_name, tuple_value, op_list)
509+
return self._send_request(request)
510+
430511
def update(self, space_name, key, op_list, **kwargs):
431512
'''
432513
Execute UPDATE request.
433-
Update single record identified by `key` (using primary index).
514+
515+
The `update` function supports operations on fields — assignment,
516+
arithmetic (if the field is unsigned numeric), cutting and pasting
517+
fragments of a field, deleting or inserting a field. Multiple
518+
operations can be combined in a single update request, and in this
519+
case they are performed atomically and sequentially. Each operation
520+
requires specification of a field number. When multiple operations are
521+
present, the field number for each operation is assumed to be relative
522+
to the most recent state of the tuple, that is, as if all previous
523+
operations in a multi-operation update have already been applied.
524+
In other words, it is always safe to merge multiple update invocations
525+
into a single invocation, with no change in semantics.
526+
527+
Update single record identified by `key`.
434528
435529
List of operations allows to update individual fields.
436530
531+
*Allowed operations:*
532+
533+
(For every operation you must provide field number, to apply this
534+
operation to)
535+
536+
* `+` for addition (values must be numeric)
537+
* `-` for subtraction (values must be numeric)
538+
* `&` for bitwise AND (values must be unsigned numeric)
539+
* `|` for bitwise OR (values must be unsigned numeric)
540+
* `^` for bitwise XOR (values must be unsigned numeric)
541+
* `:` for string splice (you must provide `offset`, `count` and `value`
542+
for this operation)
543+
* `!` for insertion (before) (provide any element to insert)
544+
* `=` for assignment (provide any element to assign)
545+
* `#` for deletion (provide count of fields to delete)
546+
437547
:param space_name: space number or name to update a record
438548
:type space_name: int or str
439549
:param index: index number or name to update a record
440550
:type index: int or str
441551
:param key: key that identifies a record
442552
:type key: int or str
443553
:param op_list: list of operations. Each operation
444-
is tuple of three values
445-
:type op_list: a list of the form
446-
[(field_1, symbol_1, arg_1), (field_2, symbol_2, arg_2),...]
554+
is tuple of three (or more) values
555+
:type op_list: a list of the form [(symbol_1, field_1, arg_1),
556+
(symbol_2, field_2, arg_2_1, arg_2_2, arg_2_3), ...]
447557
448-
:rtype: `Response` instance
558+
:rtype: ``Response`` instance
559+
560+
Operation examples:
561+
562+
.. code-block:: python
563+
564+
# 'ADD' 55 to second field
565+
# Assign 'x' to third field
566+
[('+', 2, 55), ('=', 3, 'x')]
567+
# 'OR' third field with '1'
568+
# Cut three symbols starting from second and replace them with '!!'
569+
# Insert 'hello, world' field before fifth element of tuple
570+
[('|', 3, 1), (':', 2, 2, 3, '!!'), ('!', 5, 'hello, world')]
571+
# Delete two fields starting with second field
572+
[('#', 2, 2)]
449573
'''
450574
index_name = kwargs.get("index", 0)
451575

tarantool/const.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
IPROTO_CLUSTER_UUID = 0x25
1919
IPROTO_VCLOCK = 0x26
2020
IPROTO_EXPR = 0x27
21+
IPROTO_OPS = 0x28
2122
IPROTO_DATA = 0x30
2223
IPROTO_ERROR = 0x31
2324

@@ -32,12 +33,12 @@
3233
REQUEST_TYPE_CALL = 6
3334
REQUEST_TYPE_AUTHENTICATE = 7
3435
REQUEST_TYPE_EVAL = 8
36+
REQUEST_TYPE_UPSERT = 9
3537
REQUEST_TYPE_PING = 64
3638
REQUEST_TYPE_JOIN = 65
3739
REQUEST_TYPE_SUBSCRIBE = 66
3840
REQUEST_TYPE_ERROR = 1 << 15
3941

40-
4142
SPACE_SCHEMA = 272
4243
SPACE_SPACE = 280
4344
SPACE_INDEX = 288

tarantool/request.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
IPROTO_CLUSTER_UUID,
2525
IPROTO_VCLOCK,
2626
IPROTO_EXPR,
27+
IPROTO_OPS,
2728
REQUEST_TYPE_OK,
2829
REQUEST_TYPE_PING,
2930
REQUEST_TYPE_SELECT,
3031
REQUEST_TYPE_INSERT,
3132
REQUEST_TYPE_REPLACE,
3233
REQUEST_TYPE_DELETE,
3334
REQUEST_TYPE_UPDATE,
35+
REQUEST_TYPE_UPSERT,
3436
REQUEST_TYPE_CALL,
3537
REQUEST_TYPE_EVAL,
3638
REQUEST_TYPE_AUTHENTICATE,
@@ -248,6 +250,23 @@ def __init__(self, conn):
248250
super(RequestPing, self).__init__(conn)
249251
self._bytes = self.header(0)
250252

253+
class RequestUpsert(Request):
254+
'''
255+
Represents UPSERT request
256+
'''
257+
258+
request_type = REQUEST_TYPE_UPSERT
259+
260+
# pylint: disable=W0231
261+
def __init__(self, conn, space_no, index_no, tuple_value, op_list):
262+
super(RequestUpsert, self).__init__(conn)
263+
264+
request_body = msgpack.dumps({IPROTO_SPACE_ID: space_no,
265+
IPROTO_INDEX_ID: index_no,
266+
IPROTO_TUPLE: tuple_value,
267+
IPROTO_OPS: op_list})
268+
269+
self._bytes = self.header(len(request_body)) + request_body
251270

252271
class RequestJoin(Request):
253272
'''

tarantool/space.py

Lines changed: 28 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -19,106 +19,65 @@ def __init__(self, connection, space_name):
1919
2020
:param connection: Object representing connection to the server
2121
:type connection: :class:`~tarantool.connection.Connection` instance
22-
:param int space_no: space no or name to insert a record
22+
:param int space_name: space no or name to insert a record
2323
:type space_name: int or str
2424
'''
2525

2626
self.connection = connection
2727
self.space_no = self.connection.schema.get_space(space_name).sid
2828

29-
def replace(self, values):
29+
def insert(self, *args, **kwargs):
3030
'''
31-
Execute REPLACE request.
32-
It will throw error if there's no tuple with this PK exists
33-
34-
:param values: record to be inserted. The tuple must contain
35-
only scalar (integer or strings) values
36-
:type values: tuple
31+
Execute INSERT request.
3732
38-
:rtype: :class:`~tarantool.response.Response` instance
33+
See `~tarantool.connection.insert` for more information
3934
'''
40-
self.connection.replace(self.space_no, values)
35+
return self.connection.insert(self.space_no, *args, **kwargs)
4136

42-
def insert(self, values):
37+
def replace(self, *args, **kwargs):
4338
'''
44-
Execute INSERT request.
45-
It will throw error if there's tuple with same PK exists.
46-
47-
:param values: record to be inserted. The tuple must contain
48-
only scalar (integer or strings) values
49-
:type values: tuple
39+
Execute REPLACE request.
5040
51-
:rtype: :class:`~tarantool.response.Response` instance
41+
See `~tarantool.connection.replace` for more information
5242
'''
53-
self.connection.insert(self.space_no, values)
43+
return self.connection.replace(self.space_no, *args, **kwargs)
5444

55-
def delete(self, key):
45+
def delete(self, *args, **kwargs):
5646
'''
57-
Delete records by its primary key.
47+
Execute DELETE request.
5848
59-
:param key: key of records to be deleted
60-
:type values: tuple or str or int or long
49+
See `~tarantool.connection.delete` for more information
50+
'''
51+
return self.connection.delete(self.space_no, *args, **kwargs)
6152

62-
:rtype: :class:`~tarantool.response.Response` instance
53+
def update(self, *args, **kwargs):
6354
'''
64-
return self.connection.delete(self.space_no, key)
55+
Execute UPDATE request.
6556
66-
def update(self, key, op_list):
57+
See `~tarantool.connection.update` for more information
6758
'''
68-
Update records by it's primary key with operations defined in op_list
59+
return self.connection.update(self.space_no, *args, **kwargs)
6960

70-
:param key: key of records to be updated
71-
:type values: tuple or str or int or long
61+
def upsert(self, *args, **kwargs):
62+
'''
63+
Execute UPDATE request.
7264
73-
:rtype: :class:`~tarantool.response.Response` instance
65+
See `~tarantool.connection.upsert` for more information
7466
'''
75-
return self.connection.update(self.space_no, key, op_list)
67+
return self.connection.upsert(self.space_no, *args, **kwargs)
7668

77-
def select(self, values, **kwargs):
69+
def select(self, *args, **kwargs):
7870
'''
7971
Execute SELECT request.
80-
Select and retrieve data from the database.
8172
82-
:param values: list of values to search over the index
83-
:type values: list of tuples
84-
:param index: specifies which index to use (default is **0** which
85-
means that the **primary index** will be used)
86-
:type index: int
87-
:param offset: offset in the resulting tuple set
88-
:type offset: int
89-
:param limit: limits the total number of returned tuples
90-
:type limit: int
91-
92-
:rtype: `Response` instance
73+
See `~tarantool.connection.select` for more information
9374
'''
94-
# Initialize arguments and its defaults from **kwargs
95-
# I use the explicit argument initialization from the kwargs
96-
# to make it impossible to pass positional arguments
97-
index = kwargs.get("index", 0)
98-
offset = kwargs.get("offset", 0)
99-
limit = kwargs.get("limit", 0xffffffff)
100-
101-
return self.connection.select(
102-
self.space_no, values, index=index, offset=offset, limit=limit)
75+
return self.connection.select(self.space_no, *args, **kwargs)
10376

104-
def call(self, func_name, *args, **kwargs):
77+
def call(self, *args, **kwargs):
10578
'''
10679
Execute CALL request. Call stored Lua function.
10780
108-
:param func_name: stored Lua function name
109-
:type func_name: str
110-
:param args: list of function arguments
111-
:type args: list or tuple
112-
:param field_defs: field definitions used for types conversion,
113-
e.g. [('field0', tarantool.NUM), ('field1', tarantool.STR)]
114-
:type field_defs: None or [(name, type) or None]
115-
:param default_type: None a default type used for result conversion,
116-
as defined in ``schema[space_no]['default_type']``
117-
:type default_type: None or int
118-
:param space_name: space number or name. A schema for the space
119-
will be used for type conversion.
120-
:type space_name: None or int or str
121-
122-
:rtype: `Response` instance
81+
It's deprecated, use `~tarantool.connection.call` instead
12382
'''
12483
return self.connection.call(func_name, *args, **kwargs)

0 commit comments

Comments
 (0)