From 932867650c97a4d0cf2c7121e338434b5b8c0ca4 Mon Sep 17 00:00:00 2001
From: Eugine Blikh <bigbes@gmail.com>
Date: Fri, 13 Sep 2013 04:08:33 +0400
Subject: [PATCH] Add first support of python backend for TT. Still need to add
 support of error code (now it return 0 everytime)

---
 .gitmodules                |   3 +
 test/lib/box_connection.py |  39 +++--
 test/lib/sql_ast.py        | 315 +++++++++----------------------------
 test/lib/tarantool-python  |   1 +
 4 files changed, 100 insertions(+), 258 deletions(-)
 create mode 160000 test/lib/tarantool-python

diff --git a/.gitmodules b/.gitmodules
index 89aca4dd78..118dcc251b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
 [submodule "third_party/luajit"]
 	path = third_party/luajit
 	url = http://luajit.org/git/luajit-2.0.git
+[submodule "test/lib/tarantool-python"]
+	path = test/lib/tarantool-python
+	url = https://github.com/mailru/tarantool-python.git
diff --git a/test/lib/box_connection.py b/test/lib/box_connection.py
index 2c160031f9..be93bbc60c 100644
--- a/test/lib/box_connection.py
+++ b/test/lib/box_connection.py
@@ -20,15 +20,28 @@ __author__ = "Konstantin Osipov <kostja.osipov@gmail.com>"
 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 # SUCH DAMAGE.
-
-import socket
+import os
 import sql
+import sys
+import socket
 import struct
 from tarantool_connection import TarantoolConnection
 
+try:
+    tnt_py = os.path.dirname(os.path.abspath(__file__))
+    tnt_py = os.path.join(tnt_py, 'tarantool-python/src')
+    sys.path.append(tnt_py)
+    from tarantool import Connection as tnt_connection
+except ImportError:
+    raise
+    sys.stderr.write("\n\nNo tarantool-python library found\n")
+    sys.exit(1)
+
 class BoxConnection(TarantoolConnection):
     def __init__(self, host, port):
         super(BoxConnection, self).__init__(host, port)
+        self.py_con = tnt_connection(host, port, connect_now=False)
+        self.py_con.error = False
         self.sort = False
 
     def recvall(self, length):
@@ -47,27 +60,11 @@ class BoxConnection(TarantoolConnection):
             return "You have an error in your SQL syntax\n"
         statement.sort = self.sort
 
-        payload = statement.pack()
-        header = struct.pack("<lll", statement.reqeust_type, len(payload), 0)
-
-        self.socket.sendall(header)
-        if len(payload):
-            self.socket.sendall(payload)
-
-        IPROTO_HEADER_SIZE = 12
-
-        header = self.recvall(IPROTO_HEADER_SIZE)
-
-        response_len = struct.unpack("<lll", header)[1]
-
-        if response_len:
-            response = self.recvall(response_len)
-        else:
-            response = None
+        request = statement.pack(self.py_con)
+        response = self.py_con._send_request(request, False)
 
         if not silent:
             print command
             print statement.unpack(response)
 
-        return statement.unpack(response) + "\n"
-
+        return statement.unpack(response)
diff --git a/test/lib/sql_ast.py b/test/lib/sql_ast.py
index 700811017b..d7e054e570 100644
--- a/test/lib/sql_ast.py
+++ b/test/lib/sql_ast.py
@@ -1,28 +1,25 @@
-import struct
+import os
 import re
+import sys
 import ctypes
+import struct
 
-# IPROTO header is always 3 4-byte ints:
-# command code, length, request id
-INT_FIELD_LEN = 4
-INT_BER_MAX_LEN = 5
-IPROTO_HEADER_LEN = 3*INT_FIELD_LEN
-INSERT_REQUEST_FIXED_LEN = 2*INT_FIELD_LEN
-UPDATE_REQUEST_FIXED_LEN = 2*INT_FIELD_LEN
-DELETE_REQUEST_FIXED_LEN = 2*INT_FIELD_LEN
-SELECT_REQUEST_FIXED_LEN = 5*INT_FIELD_LEN
-PACKET_BUF_LEN = 2048
-
-UPDATE_SET_FIELD_OPCODE = 0
-
-# command code in IPROTO header
-
-INSERT_REQUEST_TYPE = 13
-SELECT_REQUEST_TYPE = 17
-UPDATE_REQUEST_TYPE = 19
-DELETE_REQUEST_TYPE = 21
-CALL_REQUEST_TYPE = 22
-PING_REQUEST_TYPE = 65280
+try:
+    tnt_py = os.path.dirname(os.path.abspath(__file__))
+    tnt_py = os.path.join(tnt_py, 'tarantool-python/src')
+    sys.path.append(tnt_py)
+    from tarantool.request import (
+            RequestPing,
+            RequestInsert,
+            RequestSelect,
+            RequestCall,
+            RequestUpdate,
+            RequestDelete,
+    )
+except ImportError:
+    raise
+    sys.stderr.write("\n\nNo tarantool-python library found\n")
+    sys.exit(1)
 
 ER = {
     0: "ER_OK"                  ,
@@ -85,231 +82,100 @@ ER = {
    57: "ER_NO_SUCH_SPACE"
 }
 
-def format_error(return_code, response):
-    return "An error occurred: {0}, '{1}'".format(ER[return_code >> 8],
-                                                  response[4:-1])
-
-
-def save_varint32(value):
-    """Implement Perl pack's 'w' option, aka base 128 encoding."""
-    res = ''
-    if value >= 1 << 7:
-        if value >= 1 << 14:
-            if value >= 1 << 21:
-                if value >= 1 << 28:
-                    res += chr(value >> 28 & 0xff | 0x80)
-                res += chr(value >> 21 & 0xff | 0x80)
-            res += chr(value >> 14 & 0xff | 0x80)
-        res += chr(value >> 7 & 0xff | 0x80)
-    res += chr(value & 0x7F)
-
-    return res
-
-
-def read_varint32(varint, offset):
-    """Implement Perl unpack's 'w' option, aka base 128 decoding."""
-    res = ord(varint[offset])
-    if ord(varint[offset]) >= 0x80:
-        offset += 1
-        res = ((res - 0x80) << 7) + ord(varint[offset])
-        if ord(varint[offset]) >= 0x80:
-            offset += 1
-            res = ((res - 0x80) << 7) + ord(varint[offset])
-            if ord(varint[offset]) >= 0x80:
-                offset += 1
-                res = ((res - 0x80) << 7) + ord(varint[offset])
-                if ord(varint[offset]) >= 0x80:
-                    offset += 1
-                    res = ((res - 0x80) << 7) + ord(varint[offset])
-    return res, offset + 1
-
-
-def opt_resize_buf(buf, newsize):
-    if len(buf) < newsize:
-        return ctypes.create_string_buffer(buf.value, max(2*len, newsize))
-    return buf
-
-
-def pack_field(value, buf, offset):
-    if type(value) is int or type(value) is long:
-        if value > 0xffffffff:
-            raise RuntimeError("Integer value is too big")
-        buf = opt_resize_buf(buf, offset + INT_FIELD_LEN)
-        struct.pack_into("<cL", buf, offset, chr(INT_FIELD_LEN), value)
-        offset += INT_FIELD_LEN + 1
-    elif type(value) is str:
-        opt_resize_buf(buf, offset + INT_BER_MAX_LEN + len(value))
-        value_len_ber = save_varint32(len(value))
-        struct.pack_into("{0}s{1}s".format(len(value_len_ber), len(value)),
-                         buf, offset, value_len_ber, value)
-        offset += len(value_len_ber) + len(value)
-    else:
-        raise RuntimeError("Unsupported value type in value list")
-    return (buf, offset)
-
-
-def pack_tuple(value_list, buf, offset):
-    """Represents <tuple> rule in tarantool protocol description.
-    Pack tuple into a binary representation.
-    buf and offset are in-out parameters, offset is advanced
-    to the amount of bytes that it took to pack the tuple"""
-
-    # length of int field: 1 byte - field len (is always 4), 4 bytes - data
-    # max length of compressed integer
-    cardinality = len(value_list)
-    struct.pack_into("<L", buf, offset, cardinality)
-    offset += INT_FIELD_LEN
-    for value in value_list:
-        (buf, offset) = pack_field(value, buf, offset)
-
-    return buf, offset
-
-
-def pack_operation_list(update_list, buf, offset):
-    buf = opt_resize_buf(buf, offset + INT_FIELD_LEN)
-    struct.pack_into("<L", buf, offset, len(update_list))
-    offset += INT_FIELD_LEN
-    for update in update_list:
-        opt_resize_buf(buf, offset + INT_FIELD_LEN + 1)
-        struct.pack_into("<Lc", buf, offset,
-                         update[0],
-                         chr(UPDATE_SET_FIELD_OPCODE))
-        offset += INT_FIELD_LEN + 1
-        (buf, offset) = pack_field(update[1], buf, offset)
-
-    return (buf, offset)
-
-
-def unpack_tuple(response, offset):
-    (size, cardinality) = struct.unpack("<LL", response[offset:offset + 8])
-    offset += 8
-    res = []
-    while len(res) < cardinality:
-        (data_len, offset) = read_varint32(response, offset)
-        data = response[offset:offset+data_len]
-        offset += data_len
-        if data_len == 4:
-            (data,) = struct.unpack("<L", data)
-            res.append((str(data)))
-        else:
-            res.append("'" + data + "'")
-
-    return '[' + ', '.join(res) + ']', offset
+def format_error(response):
+    return "An error occurred: {0}".format(ER[response.return_code >> 8])
 
+class Statement(object):
+    def __init__(self):
+        pass
+    def pack(self, connection):
+        pass
+    def unpack(self, response):
+        pass
 
-class StatementPing:
-    reqeust_type = PING_REQUEST_TYPE
-    def pack(self):
-        return ""
+class StatementPing(Statement):
+    def pack(self, connection):
+        return RequestPing(connection)
 
     def unpack(self, response):
+        if response._return_code:
+            return format_error(response)
         return "ok\n---"
 
-class StatementInsert(StatementPing):
-    reqeust_type = INSERT_REQUEST_TYPE
-
+class StatementInsert(Statement):
     def __init__(self, table_name, value_list):
         self.space_no = table_name
-        self.flags = 0x02 # ADD
+        self.flags = 0x03 # ADD
         self.value_list = value_list
 
-    def pack(self):
-        buf = ctypes.create_string_buffer(PACKET_BUF_LEN)
-        (buf, offset) = pack_tuple(self.value_list, buf, INSERT_REQUEST_FIXED_LEN)
-        struct.pack_into("<LL", buf, 0, self.space_no, self.flags)
-        return buf[:offset]
+    def pack(self, connection):
+        return RequestInsert(connection, self.space_no, self.value_list, self.flags)
 
     def unpack(self, response):
-        (return_code,) = struct.unpack("<L", response[:4])
-        if return_code:
-            return format_error(return_code, response)
-        (tuple_count,) = struct.unpack("<L", response[4:8])
-        return "Insert OK, {0} row affected".format(tuple_count)
-
-class StatementReplace(StatementPing):
-    reqeust_type = INSERT_REQUEST_TYPE
+        if response._return_code:
+            return format_error(response)
+        return "Insert OK, {0} row affected".format(len(response))
 
+class StatementReplace(Statement):
     def __init__(self, table_name, value_list):
         self.space_no = table_name
-        self.flags = 0x04 # REPLACE
+        self.flags = 0x05 # REPLACE
         self.value_list = value_list
 
-    def pack(self):
-        buf = ctypes.create_string_buffer(PACKET_BUF_LEN)
-        (buf, offset) = pack_tuple(self.value_list, buf, INSERT_REQUEST_FIXED_LEN)
-        struct.pack_into("<LL", buf, 0, self.space_no, self.flags)
-        return buf[:offset]
+    def pack(self, connection):
+        return RequestInsert(connection, self.space_no, self.value_list, self.flags)
 
     def unpack(self, response):
-        (return_code,) = struct.unpack("<L", response[:4])
-        if return_code:
-            return format_error(return_code, response)
-        (tuple_count,) = struct.unpack("<L", response[4:8])
-        return "Replace OK, {0} row affected".format(tuple_count)
-
-class StatementUpdate(StatementPing):
-    reqeust_type = UPDATE_REQUEST_TYPE
+        if response._return_code:
+            return format_error(response)
+        return "Insert OK, {0} row affected".format(len(response))
 
+class StatementUpdate(Statement):
     def __init__(self, table_name, update_list, where):
         self.space_no = table_name
         self.flags = 0
         key_no = where[0]
         if key_no != 0:
             raise RuntimeError("UPDATE can only be made by the primary key (#0)")
-        self.value_list = where[1:]
-        self.update_list = update_list
+        self.value_list = where[1]
+        self.update_list = [(pair[0], '=', pair[1]) for pair in update_list]
 
-    def pack(self):
-        buf = ctypes.create_string_buffer(PACKET_BUF_LEN)
-        struct.pack_into("<LL", buf, 0, self.space_no, self.flags)
-        (buf, offset) = pack_tuple(self.value_list, buf, UPDATE_REQUEST_FIXED_LEN)
-        (buf, offset) = pack_operation_list(self.update_list, buf, offset)
-        return buf[:offset]
+    def pack(self, connection):
+        return RequestUpdate(connection, self.space_no, self.value_list, True)
 
     def unpack(self, response):
-        (return_code,) = struct.unpack("<L", response[:4])
-        if return_code:
-            return format_error(return_code, response)
-        (tuple_count,) = struct.unpack("<L", response[4:8])
-        return "Update OK, {0} row affected".format(tuple_count)
-
-class StatementDelete(StatementPing):
-    reqeust_type = DELETE_REQUEST_TYPE
+        if response._return_code:
+            return format_error(response)
+        return "Update OK, {0} row affected".format(len(response))
 
+class StatementDelete(Statement):
     def __init__(self, table_name, where):
         self.space_no = table_name
         self.flags = 0
         key_no = where[0]
         if key_no != 0:
             raise RuntimeError("DELETE can only be made by the primary key (#0)")
-        self.value_list = where[1:]
+        self.value_list = where[1]
 
-    def pack(self):
-        buf = ctypes.create_string_buffer(PACKET_BUF_LEN)
-        (buf, offset) = pack_tuple(self.value_list, buf, DELETE_REQUEST_FIXED_LEN)
-        struct.pack_into("<LL", buf, 0, self.space_no, self.flags)
-        return buf[:offset]
+    def pack(self, connection):
+        return RequestDelete(connection, self.space_no, self.value_list, True)
 
     def unpack(self, response):
-        (return_code,) = struct.unpack("<L", response[:4])
-        if return_code:
-            return format_error(return_code, response)
-        (tuple_count,) = struct.unpack("<L", response[4:8])
-        return "Delete OK, {0} row affected".format(tuple_count)
-
-class StatementSelect(StatementPing):
-    reqeust_type = SELECT_REQUEST_TYPE
+        if response._return_code:
+            return format_error(response)
+        return "Delete OK, {0} row affected".format(len(response))
 
+class StatementSelect(Statement):
     def __init__(self, table_name, where, limit):
         self.space_no = table_name
         self.index_no = None
         self.key_list = []
         if not where:
             self.index_no = 0
-            self.key_list = ["",]
+            self.key_list = [[]]
         else:
             for (index_no, key) in where:
-                self.key_list.append(key)
+                self.key_list.append([key, ])
                 if self.index_no == None:
                     self.index_no = index_no
                 elif self.index_no != index_no:
@@ -317,54 +183,29 @@ class StatementSelect(StatementPing):
         self.offset = 0
         self.limit = limit
 
-    def pack(self):
-        buf = ctypes.create_string_buffer(PACKET_BUF_LEN)
-        struct.pack_into("<LLLLL", buf, 0,
-                         self.space_no,
-                         self.index_no,
-                         self.offset,
-                         self.limit,
-                         len(self.key_list))
-        offset = SELECT_REQUEST_FIXED_LEN
-
-        for key in self.key_list:
-            (buf, offset) = pack_tuple([key], buf, offset)
-
-        return buf[:offset]
+    def pack(self, connection):
+        return RequestSelect(connection, self.space_no, self.index_no, 
+                self.key_list , self.offset, self.limit)
 
     def unpack(self, response):
-        (return_code,) = struct.unpack("<L", response[:4])
-        if return_code:
-            return format_error(return_code, response)
-        (tuple_count,) = struct.unpack("<L", response[4:8])
-        tuples = []
-        offset = 8
-        while len(tuples) < tuple_count:
-            (next_tuple, offset) = unpack_tuple(response, offset)
-            tuples.append(next_tuple)
+        if response._return_code:
+            return format_error(response)
         if self.sort:
-            tuples.sort()
-        if tuple_count == 0:
+            response = sorted(response[0:])
+        if not len(response):
             return "No match"
-        elif tuple_count == 1:
-            return "Found 1 tuple:\n" + tuples[0]
+        elif len(response) == 1:
+            return "Found 1 tuple:\n" + str(response[0])
         else:
-            return "Found {0} tuples:\n".format(tuple_count) + "\n".join(tuples)
+            return "Found {0} tuples:\n".format(len(response)) + \
+                    "\n".join([str(tup) for tup in response])
 
 class StatementCall(StatementSelect):
-    reqeust_type = CALL_REQUEST_TYPE
-
     def __init__(self, proc_name, value_list):
         self.proc_name = proc_name
 # the binary protocol passes everything into procedure as strings
 # convert input to strings to avoid data mangling by the protocol
         self.value_list = map(lambda val: str(val), value_list)
 
-    def pack(self):
-        buf = ctypes.create_string_buffer(PACKET_BUF_LEN)
-        offset = 0
-        struct.pack_into("<L", buf, offset, 0) # flags
-        offset += INT_FIELD_LEN
-        (buf, offset) = pack_field(self.proc_name, buf, offset)
-        (buf, offset) = pack_tuple(self.value_list, buf, offset)
-        return buf[:offset]
+    def pack(self, connection):
+        return RequestCall(connection, self.proc_name, self.value_list, True)
diff --git a/test/lib/tarantool-python b/test/lib/tarantool-python
new file mode 160000
index 0000000000..e3e3f6f284
--- /dev/null
+++ b/test/lib/tarantool-python
@@ -0,0 +1 @@
+Subproject commit e3e3f6f28429900850ed7af7b9b481002e1b1894
-- 
GitLab