diff --git a/test/box/sql.result b/test/box/sql.result
index 93c819eb2507348d38919d0272b6b151a81f2613..27ace669657c18c0d7d46ad0d53c7814651fdb6c 100644
--- a/test/box/sql.result
+++ b/test/box/sql.result
@@ -23,3 +23,33 @@ Found 1 tuple:
 select * from t0 where k0 = 1
 Found 1 tuple:
 [1, 'I am a tuple']
+delete from t0 where k0 = 1
+Delete OK, 1 row affected
+select * from t0 where k0 = 1
+No match
+update t0 set k1 = "I am a new tuple" where k0=1
+Insert OK, 0 row affected
+select * from t0 where k0 = 1
+No match
+insert into t0 values (1, "I am a new tuple")
+Insert OK, 1 row affected
+select * from t0 where k0 = 1
+Found 1 tuple:
+[1, 'I am a new tuple']
+update t0 set k1 = "I am the newest tuple" where k0=1
+Insert OK, 1 row affected
+select * from t0 where k0 = 1
+Found 1 tuple:
+[1, 'I am the newest tuple']
+update t0 set k1 = "Huh", k2 = "Oh-ho-ho" where k0=1
+An error occurred: ERR_CODE_ILLEGAL_PARAMS, 'Illegal parameters'
+select * from t0 where k0 = 1
+Found 1 tuple:
+[1, 'I am the newest tuple']
+insert into t0 values (1, "I am a new tuple", "stub")
+Insert OK, 1 row affected
+update t0 set k1 = "Huh", k2 = "Oh-ho-ho" where k0=1
+Insert OK, 1 row affected
+select * from t0 where k0 = 1
+Found 1 tuple:
+[1, 'Huh', 'Oh-ho-ho']
diff --git a/test/box/sql.test b/test/box/sql.test
index ba955f39de372a1b3f64ae1a00b0c45407eab346..c0a040bd691283e76a2beadd17ab56172b21fe2a 100644
--- a/test/box/sql.test
+++ b/test/box/sql.test
@@ -14,5 +14,19 @@ exec admin "save snapshot"
 exec sql 'select * from t0 where k0 = 1'
 server.restart()
 exec sql 'select * from t0 where k0 = 1'
+exec sql 'delete from t0 where k0 = 1'
+exec sql 'select * from t0 where k0 = 1'
+exec sql 'update t0 set k1 = "I am a new tuple" where k0=1'
+exec sql 'select * from t0 where k0 = 1'
+exec sql 'insert into t0 values (1, "I am a new tuple")'
+exec sql 'select * from t0 where k0 = 1'
+exec sql 'update t0 set k1 = "I am the newest tuple" where k0=1'
+exec sql 'select * from t0 where k0 = 1'
+# this is illegal, can't change tuple dimension with update
+exec sql 'update t0 set k1 = "Huh", k2 = "Oh-ho-ho" where k0=1'
+exec sql 'select * from t0 where k0 = 1'
+exec sql 'insert into t0 values (1, "I am a new tuple", "stub")'
+exec sql 'update t0 set k1 = "Huh", k2 = "Oh-ho-ho" where k0=1'
+exec sql 'select * from t0 where k0 = 1'
 
 # vim: syntax=python
diff --git a/test/lib/sql.g b/test/lib/sql.g
index a57fdd50012a68f803417314d496252f4c653bc8..594e9942fe2c5091d3ab2151b58beda797100475 100644
--- a/test/lib/sql.g
+++ b/test/lib/sql.g
@@ -1,4 +1,7 @@
 import sql_ast
+import re
+
+object_no_re = re.compile("[a-z_]*", re.I)
 
 %%
 
@@ -26,18 +29,18 @@ parser sql:
                       select {{ stmt = select }} |
                       ping {{ stmt = ping }}) END {{ return stmt }}
                       
-    rule insert:      INSERT [INTO] ID VALUES value_list
-                      {{ return sql_ast.StatementInsert(ID, value_list) }}
-    rule update:      UPDATE ID SET update_list opt_where 
-                      {{ return sql_ast.StatementUpdate(ID, update_list, opt_where) }}
-    rule delete:      DELETE FROM ID  opt_where
-                      {{ return sql_ast.StatementDelete(ID, opt_where) }}
-    rule select:      SELECT '\*' FROM ID opt_where
-                      {{ return sql_ast.StatementSelect(ID, opt_where) }}
+    rule insert:      INSERT [INTO] ident VALUES value_list
+                      {{ return sql_ast.StatementInsert(ident, value_list) }}
+    rule update:      UPDATE ident SET update_list opt_where 
+                      {{ return sql_ast.StatementUpdate(ident, update_list, opt_where) }}
+    rule delete:      DELETE FROM ident opt_where
+                      {{ return sql_ast.StatementDelete(ident, opt_where) }}
+    rule select:      SELECT '\*' FROM ident opt_where
+                      {{ return sql_ast.StatementSelect(ident, opt_where) }}
     rule ping:        PING
                       {{ return sql_ast.StatementPing() }}
-    rule predicate:   ID '=' constant
-                      {{ return (ID, constant) }}
+    rule predicate:   ident '=' constant
+                      {{ return (ident, constant) }}
     rule opt_where:   {{ return None }}
                       | WHERE predicate
                       {{ return predicate }}
@@ -49,6 +52,7 @@ parser sql:
                       {{ return update_list }}
     rule expr:        constant {{ return constant }}
     rule constant:    NUM {{ return int(NUM) }} | STR {{ return STR[1:-1] }}
+    rule ident:       ID {{ return int(object_no_re.sub("", ID)) }}
 %%
 
 # SQL is case-insensitive, but in yapps it's not possible to
diff --git a/test/lib/sql.py b/test/lib/sql.py
index 80f87134b025006f3008dc2084f7e40a300c56b1..93593316b3b447c98a30a9496a46b06b5ce9e1ef 100644
--- a/test/lib/sql.py
+++ b/test/lib/sql.py
@@ -1,4 +1,7 @@
 import sql_ast
+import re
+
+object_no_re = re.compile("[a-z_]*", re.I)
 
 
 # Begin -- grammar generated by Yapps
@@ -60,36 +63,36 @@ class sql(runtime.Parser):
         INSERT = self._scan('INSERT', context=_context)
         if self._peek('INTO', 'ID', context=_context) == 'INTO':
             INTO = self._scan('INTO', context=_context)
-        ID = self._scan('ID', context=_context)
+        ident = self.ident(_context)
         VALUES = self._scan('VALUES', context=_context)
         value_list = self.value_list(_context)
-        return sql_ast.StatementInsert(ID, value_list)
+        return sql_ast.StatementInsert(ident, value_list)
 
     def update(self, _parent=None):
         _context = self.Context(_parent, self._scanner, 'update', [])
         UPDATE = self._scan('UPDATE', context=_context)
-        ID = self._scan('ID', context=_context)
+        ident = self.ident(_context)
         SET = self._scan('SET', context=_context)
         update_list = self.update_list(_context)
         opt_where = self.opt_where(_context)
-        return sql_ast.StatementUpdate(ID, update_list, opt_where)
+        return sql_ast.StatementUpdate(ident, update_list, opt_where)
 
     def delete(self, _parent=None):
         _context = self.Context(_parent, self._scanner, 'delete', [])
         DELETE = self._scan('DELETE', context=_context)
         FROM = self._scan('FROM', context=_context)
-        ID = self._scan('ID', context=_context)
+        ident = self.ident(_context)
         opt_where = self.opt_where(_context)
-        return sql_ast.StatementDelete(ID, opt_where)
+        return sql_ast.StatementDelete(ident, opt_where)
 
     def select(self, _parent=None):
         _context = self.Context(_parent, self._scanner, 'select', [])
         SELECT = self._scan('SELECT', context=_context)
         self._scan("'\\*'", context=_context)
         FROM = self._scan('FROM', context=_context)
-        ID = self._scan('ID', context=_context)
+        ident = self.ident(_context)
         opt_where = self.opt_where(_context)
-        return sql_ast.StatementSelect(ID, opt_where)
+        return sql_ast.StatementSelect(ident, opt_where)
 
     def ping(self, _parent=None):
         _context = self.Context(_parent, self._scanner, 'ping', [])
@@ -98,10 +101,10 @@ class sql(runtime.Parser):
 
     def predicate(self, _parent=None):
         _context = self.Context(_parent, self._scanner, 'predicate', [])
-        ID = self._scan('ID', context=_context)
+        ident = self.ident(_context)
         self._scan("'='", context=_context)
         constant = self.constant(_context)
-        return (ID, constant)
+        return (ident, constant)
 
     def opt_where(self, _parent=None):
         _context = self.Context(_parent, self._scanner, 'opt_where', [])
@@ -154,6 +157,11 @@ class sql(runtime.Parser):
             STR = self._scan('STR', context=_context)
             return STR[1:-1]
 
+    def ident(self, _parent=None):
+        _context = self.Context(_parent, self._scanner, 'ident', [])
+        ID = self._scan('ID', context=_context)
+        return int(object_no_re.sub("", ID))
+
 
 def parse(rule, text):
     P = sql(sqlScanner(text))
diff --git a/test/lib/sql_ast.py b/test/lib/sql_ast.py
index bfe70ab3dcf1b6f3d05828a4d7419864a5537c24..fa0e04e50bb2ea777a6676a57d1eb6ea4a7bbea3 100644
--- a/test/lib/sql_ast.py
+++ b/test/lib/sql_ast.py
@@ -4,11 +4,17 @@ import ctypes
 
 # IPROTO header is always 3 4-byte ints:
 # command code, length, request id
-IPROTO_HEADER_LEN = 12
-INSERT_REQUEST_FIXED_LEN = 8
-SELECT_REQUEST_FIXED_LEN = 20
+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 = 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
@@ -51,7 +57,6 @@ def format_error(return_code):
   return "An error occurred: {0}, \'{1}'".format(ER[return_code][0],
                                                  ER[return_code][1])
 
-object_no_re = re.compile("[a-z_]*", re.I)
 
 def save_varint32(value):
   """Implement Perl pack's 'w' option, aka base 128 encoding."""
@@ -92,29 +97,48 @@ def opt_resize_buf(buf, newsize):
   return buf
 
 
+def pack_field(value, buf, offset):
+  if type(value) is int:
+    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
-  INT_FIELD_LEN = 4
   # max length of compressed integer
-  INT_BER_MAX_LEN = 5
   cardinality = len(value_list)
   struct.pack_into("<L", buf, offset, cardinality)
   offset += INT_FIELD_LEN
   for value in value_list:
-    if type(value) is int:
-      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")
+    (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])
@@ -128,6 +152,7 @@ def unpack_tuple(response, offset):
       (data,) = struct.unpack("<L", data)
     res.append(data)
   return str(res), offset
+
    
 class StatementPing:
   reqeust_type = PING_REQUEST_TYPE
@@ -141,7 +166,7 @@ class StatementInsert(StatementPing):
   reqeust_type = INSERT_REQUEST_TYPE
 
   def __init__(self, table_name, value_list):
-    self.namespace_no = int(object_no_re.sub("", table_name))
+    self.namespace_no = table_name
     self.flags = 0
     self.value_list = value_list
 
@@ -163,25 +188,58 @@ class StatementUpdate(StatementPing):
   reqeust_type = UPDATE_REQUEST_TYPE
 
   def __init__(self, table_name, update_list, where):
-    self.namespace_no = int(object_no_re.sub("", table_name))
+    self.namespace_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.where = where
+
+  def pack(self):
+    buf = ctypes.create_string_buffer(PACKET_BUF_LEN)
+    struct.pack_into("<LL", buf, 0, self.namespace_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 unpack(self, response):
+    (return_code,) = struct.unpack("<L", response[:4])
+    if return_code:
+      return format_error(return_code)
+    (result_code, row_count) = struct.unpack("<LL", response)
+    return "Insert OK, {0} row affected".format(row_count)
 
 class StatementDelete(StatementPing):
   reqeust_type = DELETE_REQUEST_TYPE
 
   def __init__(self, table_name, where):
-    self.namespace_no = int(object_no_re.sub("", table_name))
-    self.where = where
+    self.namespace_no = table_name
+    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:]
+
+  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("<L", buf, 0, self.namespace_no)
+    return buf[:offset]
+
+  def unpack(self, response):
+    (return_code,) = struct.unpack("<L", response[:4])
+    if return_code:
+      return format_error(return_code)
+    (result_code, row_count) = struct.unpack("<LL", response)
+    return "Delete OK, {0} row affected".format(row_count)
 
 class StatementSelect(StatementPing):
   reqeust_type = SELECT_REQUEST_TYPE
 
   def __init__(self, table_name, where):
-    self.namespace_no = int(object_no_re.sub("", table_name))
+    self.namespace_no = table_name
     if where:
-      (index, key) = where
-      self.index_no = int(object_no_re.sub("", index))
+      (self.index_no, key) = where
       self.key = [key]
     else:
       self.index_no = 0
diff --git a/test/test-run.py b/test/test-run.py
index 9ae553d9c6d5b7183fbb79b6a9c0283971c0d450..4321b2e3be520e8b1ed0bcf80ff4392f8d4c1f8e 100755
--- a/test/test-run.py
+++ b/test/test-run.py
@@ -116,7 +116,7 @@ class Options:
         help = """Run test suite in memory, using tmpfs or ramdisk.
         Is used only if vardir is not an absolute path. In that case
         vardir is sym-linked to /dev/shm/<vardir>.
-        Linux only. Default: true""")
+        Linux only. Default: false""")
 
     self.check(parser)
     self.args = parser.parse_args()