diff --git a/sbroad-cartridge/src/cartridge/config/tests.rs b/sbroad-cartridge/src/cartridge/config/tests.rs
index 84abf36ad6d5789ad5442cc610ce33bc65fe9088..455fbc968f988e88754c50f967fff7f7b18e9e9c 100644
--- a/sbroad-cartridge/src/cartridge/config/tests.rs
+++ b/sbroad-cartridge/src/cartridge/config/tests.rs
@@ -111,6 +111,9 @@ fn test_getting_table_segment() {
       - name: \"sys_op\"
         type: \"number\"
         is_nullable: false
+      - name: detail
+        type: array
+        is_nullable: false
       - name: \"bucket_id\"
         type: \"unsigned\"
         is_nullable: true
@@ -144,6 +147,7 @@ fn test_getting_table_segment() {
             Column::new("\"product_code\"", Type::String, ColumnRole::User),
             Column::new("\"product_units\"", Type::Boolean, ColumnRole::User),
             Column::new("\"sys_op\"", Type::Number, ColumnRole::User),
+            Column::new("\"detail\"", Type::Array, ColumnRole::User),
             Column::new("\"bucket_id\"", Type::Unsigned, ColumnRole::Sharding),
         ],
         &["\"identification_number\"", "\"product_code\""],
@@ -166,11 +170,12 @@ fn test_waiting_timeout() {
 
     assert_eq!(s.get_exec_waiting_timeout(), 200);
 }
+
 #[test]
 fn test_invalid_schema() {
     let test_schema = r#"spaces:
       TEST_SPACE:
-        engine: vinyl
+        engine: memtx
         is_local: false
         temporary: false
         format:
@@ -196,7 +201,7 @@ fn test_invalid_schema() {
             type: string
             is_nullable: false
           - name: COMMON_DETAIL
-            type: array
+            type: map
             is_nullable: false
           - name: TYPOLOGY_TYPE
             type: integer
@@ -236,6 +241,6 @@ fn test_invalid_schema() {
 
     assert_eq!(
         s.load_schema(test_schema).unwrap_err(),
-        QueryPlannerError::CustomError("type `array` not implemented".into())
+        QueryPlannerError::CustomError("type `map` not implemented".into())
     );
 }
diff --git a/sbroad-cartridge/test_app/app/roles/api.lua b/sbroad-cartridge/test_app/app/roles/api.lua
index 1d9fc1f736df2bfc88e1531071bd42f4598525b0..96645796c5f9784b87abe8f37040fc8a3763cb85 100644
--- a/sbroad-cartridge/test_app/app/roles/api.lua
+++ b/sbroad-cartridge/test_app/app/roles/api.lua
@@ -29,6 +29,7 @@ return {
     role_name = 'app.roles.api',
     init = init,
     dependencies = {
-        'cartridge.roles.sbroad-router'
+        'cartridge.roles.sbroad-router',
+        'cartridge.roles.crud-router',
     },
 }
diff --git a/sbroad-cartridge/test_app/app/roles/storage.lua b/sbroad-cartridge/test_app/app/roles/storage.lua
index 99802aed535b556dcb3d09f86ebab974b8315896..d45b3edc3a882f31778984151ce312a0541f62d7 100644
--- a/sbroad-cartridge/test_app/app/roles/storage.lua
+++ b/sbroad-cartridge/test_app/app/roles/storage.lua
@@ -15,6 +15,7 @@ return {
     role_name = 'app.roles.storage',
     init = init,
     dependencies = {
-        'cartridge.roles.sbroad-storage'
+        'cartridge.roles.sbroad-storage',
+        'cartridge.roles.crud-storage',
     },
 }
diff --git a/sbroad-cartridge/test_app/app/utils.lua b/sbroad-cartridge/test_app/app/utils.lua
new file mode 100644
index 0000000000000000000000000000000000000000..cdbaf869f8dc38e4d320f63c2d1eac15bb441a5c
--- /dev/null
+++ b/sbroad-cartridge/test_app/app/utils.lua
@@ -0,0 +1,19 @@
+local function crud_sharding_func(shard_val)
+    if type(shard_val) ~= 'table' then
+        -- luacheck: ignore sbroad
+        return sbroad.calculate_bucket_id(tostring(shard_val))
+    end
+
+    local string_value = ''
+    for _, v in ipairs(shard_val) do
+        string_value = string_value .. tostring(v)
+    end
+
+    -- luacheck: ignore sbroad
+    return sbroad.calculate_bucket_id(string_value)
+
+end
+
+return {
+  crud_sharding_func = crud_sharding_func
+}
diff --git a/sbroad-cartridge/test_app/init.lua b/sbroad-cartridge/test_app/init.lua
index 84749a3098b5e74bbc40ea644f7b49b4ef471092..0f0b4c7a338c05ed808cef8653eff2ddff7a1e86 100755
--- a/sbroad-cartridge/test_app/init.lua
+++ b/sbroad-cartridge/test_app/init.lua
@@ -29,6 +29,9 @@ else
 end
 
 -- configure cartridge
+-- configure cartridge
+local utils = require('app.utils')
+rawset(_G, "crud_sharding_func", utils.crud_sharding_func )
 
 local cartridge = require('cartridge')
 
diff --git a/sbroad-cartridge/test_app/test/integration/array_test.lua b/sbroad-cartridge/test_app/test/integration/array_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..81a8eaf5634c9feaeb7d3dc5412198ccd51aa161
--- /dev/null
+++ b/sbroad-cartridge/test_app/test/integration/array_test.lua
@@ -0,0 +1,96 @@
+local t = require('luatest')
+local g = t.group('array_field')
+local helper = require('test.helper')
+
+g.before_all(
+    function()
+      helper.stop_test_cluster()
+
+      local cfg = {
+        schema = {
+          spaces = {
+            t = {
+              format = {
+                {
+                  name = "id",
+                  type = "integer",
+                  is_nullable = false,
+                },
+                {
+                  name = "a",
+                  type = "array",
+                  is_nullable = false,
+                },
+                {
+                  name = "bucket_id",
+                  type = "unsigned",
+                  is_nullable = true,
+                },
+              },
+              temporary = false,
+              engine = "memtx",
+              indexes = {
+                {
+                  unique = true,
+                  parts = {
+                    {
+                      path = "id",
+                      type = "integer",
+                      is_nullable = false,
+                    },
+                  },
+                  type = "TREE",
+                  name = "id",
+                },
+                {
+                  unique = false,
+                  parts = {
+                    {
+                      path = "bucket_id",
+                      type = "unsigned",
+                      is_nullable = true,
+                    },
+                  },
+                  type = "TREE",
+                  name = "bucket_id",
+                },
+              },
+              is_local = false,
+              sharding_key = { "id" },
+              sharding_func = "crud_sharding_func"
+            },
+          }
+        }
+      }
+
+      helper.start_test_cluster(cfg)
+
+      local api = helper.cluster:server("api-1").net_box
+      local _, err = api:call('crud.insert', {'t', {1, { 1, 2, 'a' }, box.NULL}})
+      t.assert_equals(err, nil)
+    end
+)
+
+g.after_all(
+    function()
+      helper.stop_test_cluster()
+
+      helper.start_test_cluster(helper.cluster_config)
+    end
+)
+
+g.test_array_read = function ()
+  local api = helper.cluster:server("api-1").net_box
+
+  local r, err = api:call("sbroad.execute", { [[select "id", "a" from "t"]], {}})
+  t.assert_equals(err, nil)
+  t.assert_equals(r, {
+        metadata = {
+            {name = "id", type = "integer"},
+            {name = "a", type = "array"},
+        },
+        rows = {
+          { 1, { 1, 2, 'a' }}
+        },
+    })
+end
diff --git a/sbroad-cartridge/test_app/test/integration/validate_schema_test.lua b/sbroad-cartridge/test_app/test/integration/validate_schema_test.lua
index ed577a1b6d8cf4cf78681003b76260e79736599b..0ed70827e05e4076fc4a66325f63f9e4bfb4f383 100644
--- a/sbroad-cartridge/test_app/test/integration/validate_schema_test.lua
+++ b/sbroad-cartridge/test_app/test/integration/validate_schema_test.lua
@@ -18,7 +18,7 @@ g.before_all(
                 },
                 {
                   name = "a",
-                  type = "array",
+                  type = "map",
                   is_nullable = false,
                 },
                 {
@@ -79,5 +79,5 @@ g.test_schema_invalid = function ()
   local api = helper.cluster:server("api-1").net_box
 
   local _, err = api:call("sbroad.execute", { [[select * from "t"]], {}})
-  t.assert_str_contains(tostring(err), "Failed to get configuration: type `array` not implemented")
+  t.assert_str_contains(tostring(err), "Failed to get configuration: type `map` not implemented")
 end
diff --git a/sbroad-cartridge/test_app/test_app-scm-1.rockspec b/sbroad-cartridge/test_app/test_app-scm-1.rockspec
index 595408dff364bfc7f77e0f09d4c36ba1d6ae6cdf..b4b071aa70680b1762a07b570aa1e3e1efb393d4 100644
--- a/sbroad-cartridge/test_app/test_app-scm-1.rockspec
+++ b/sbroad-cartridge/test_app/test_app-scm-1.rockspec
@@ -10,6 +10,7 @@ dependencies = {
     'checks == 3.1.0-1',
     'cartridge == 2.7.5-1',
     'metrics == 0.14.0-1',
+    'crud == 0.14.0-1',
     'cartridge-cli-extensions == 1.1.1-1',
     'luatest',
     'luacov'
diff --git a/sbroad-core/src/ir/relation.rs b/sbroad-core/src/ir/relation.rs
index 0ec2678eb4d2f91655ebe8de1fbb82e1e36a0d90..4417003e60f2633d9073b0b0502d3ac91a29ef0a 100644
--- a/sbroad-core/src/ir/relation.rs
+++ b/sbroad-core/src/ir/relation.rs
@@ -16,7 +16,8 @@ use super::distribution::Key;
 
 const DEFAULT_VALUE: Value = Value::Null;
 
-/// Supported column types.
+/// Supported column types, which is used in a schema only.
+/// This `Type` doesn't have any relation with `Type` from IR.
 #[derive(LuaRead, Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]
 pub enum Type {
     Boolean,
@@ -27,6 +28,7 @@ pub enum Type {
     Scalar,
     String,
     Unsigned,
+    Array,
 }
 
 impl Type {
@@ -44,6 +46,7 @@ impl Type {
             "scalar" => Ok(Type::Scalar),
             "string" => Ok(Type::String),
             "unsigned" => Ok(Type::Unsigned),
+            "array" => Ok(Type::Array),
             v => Err(QueryPlannerError::CustomError(format!(
                 "type `{}` not implemented",
                 v
@@ -96,6 +99,7 @@ impl SerSerialize for Column {
             Type::Scalar => map.serialize_entry("type", "scalar")?,
             Type::String => map.serialize_entry("type", "string")?,
             Type::Unsigned => map.serialize_entry("type", "unsigned")?,
+            Type::Array => map.serialize_entry("type", "array")?,
         }
 
         map.end()
@@ -136,6 +140,7 @@ impl<'de> Visitor<'de> for ColumnVisitor {
                 Ok(Column::new(&column_name, Type::String, ColumnRole::User))
             }
             "unsigned" => Ok(Column::new(&column_name, Type::Unsigned, ColumnRole::User)),
+            "array" => Ok(Column::new(&column_name, Type::Array, ColumnRole::User)),
             _ => Err(Error::custom("unsupported column type")),
         }
     }
diff --git a/sbroad-core/src/ir/value.rs b/sbroad-core/src/ir/value.rs
index e3950fef7d41e5be19707af9d4c0c2cc9451b913..72c2653fddb027f1037c544029d769b2c705708a 100644
--- a/sbroad-core/src/ir/value.rs
+++ b/sbroad-core/src/ir/value.rs
@@ -1,6 +1,6 @@
 //! Value module.
 
-use std::fmt;
+use std::fmt::{self, Display};
 use std::hash::Hash;
 use std::num::NonZeroI32;
 use std::str::FromStr;
@@ -10,9 +10,83 @@ use serde::{Deserialize, Serialize};
 use tarantool::decimal::Decimal;
 use tarantool::tlua;
 
+use crate::error;
 use crate::executor::hash::ToHashString;
 use crate::ir::value::double::Double;
 
+#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone)]
+pub struct Tuple(Vec<Value>);
+
+impl Display for Tuple {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(
+            f,
+            "[{}]",
+            self.0
+                .iter()
+                .map(ToString::to_string)
+                .collect::<Vec<String>>()
+                .join(",")
+        )
+    }
+}
+
+impl From<Vec<Value>> for Tuple {
+    fn from(v: Vec<Value>) -> Self {
+        Tuple(v)
+    }
+}
+
+impl<L: tlua::AsLua> tlua::Push<L> for Tuple {
+    type Err = tlua::Void;
+
+    #[allow(unreachable_code)]
+    fn push_to_lua(&self, lua: L) -> Result<tlua::PushGuard<L>, (Self::Err, L)> {
+        match self.0.push_to_lua(lua) {
+            Ok(r) => Ok(r),
+            Err(e) => {
+                error!(Option::from("push ir tuple to lua"), &format!("{:?}", e.0),);
+                Err((tlua::Void::from(e.0), e.1))
+            }
+        }
+    }
+}
+
+impl<L> tlua::PushInto<L> for Tuple
+where
+    L: tlua::AsLua,
+{
+    type Err = tlua::Void;
+
+    #[allow(unreachable_code)]
+    fn push_into_lua(self, lua: L) -> Result<tlua::PushGuard<L>, (Self::Err, L)> {
+        match self.0.push_into_lua(lua) {
+            Ok(r) => Ok(r),
+            Err(e) => {
+                error!(
+                    Option::from("push ir tuple into lua"),
+                    &format!("{:?}", e.0),
+                );
+                Err((tlua::Void::from(e.0), e.1))
+            }
+        }
+    }
+}
+
+impl<L> tlua::PushOneInto<L> for Tuple where L: tlua::AsLua {}
+
+impl<L> tlua::LuaRead<L> for Tuple
+where
+    L: tlua::AsLua,
+{
+    fn lua_read_at_position(lua: L, index: NonZeroI32) -> Result<Tuple, L> {
+        match Vec::lua_read_at_position(lua, index) {
+            Ok(v) => Ok(Tuple::from(v)),
+            Err(lua) => Err(lua),
+        }
+    }
+}
+
 /// SQL uses three-valued logic. We need to implement
 /// it to compare values with each other.
 #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
@@ -50,6 +124,8 @@ pub enum Value {
     String(String),
     /// Unsigned integer type.
     Unsigned(u64),
+    /// Tuple type
+    Tuple(Tuple),
 }
 
 /// As a side effect, `NaN == NaN` is true.
@@ -66,6 +142,7 @@ impl fmt::Display for Value {
             Value::Double(v) => fmt::Display::fmt(&v, f),
             Value::Decimal(v) => fmt::Display::fmt(v, f),
             Value::String(v) => write!(f, "'{}'", v),
+            Value::Tuple(v) => write!(f, "{}", v),
         }
     }
 }
@@ -133,6 +210,19 @@ impl From<f64> for Value {
     }
 }
 
+impl From<Tuple> for Value {
+    fn from(v: Tuple) -> Self {
+        Value::Tuple(v)
+    }
+}
+
+impl From<Vec<Value>> for Value {
+    fn from(v: Vec<Value>) -> Self {
+        let t = Tuple::from(v);
+        Value::Tuple(t)
+    }
+}
+
 impl From<Trivalent> for Value {
     fn from(f: Trivalent) -> Self {
         match f {
@@ -156,11 +246,12 @@ impl Value {
                 | Value::Integer(_)
                 | Value::Decimal(_)
                 | Value::Double(_)
-                | Value::String(_) => Trivalent::False,
+                | Value::String(_)
+                | Value::Tuple(_) => Trivalent::False,
             },
             Value::Null => Trivalent::Unknown,
             Value::Integer(s) => match other {
-                Value::Boolean(_) | Value::String(_) => Trivalent::False,
+                Value::Boolean(_) | Value::String(_) | Value::Tuple(_) => Trivalent::False,
                 Value::Null => Trivalent::Unknown,
                 Value::Integer(o) => (s == o).into(),
                 Value::Decimal(o) => (&Decimal::from(*s) == o).into(),
@@ -171,7 +262,7 @@ impl Value {
                 Value::Unsigned(o) => (&Decimal::from(*s) == o).into(),
             },
             Value::Double(s) => match other {
-                Value::Boolean(_) | Value::String(_) => Trivalent::False,
+                Value::Boolean(_) | Value::String(_) | Value::Tuple(_) => Trivalent::False,
                 Value::Null => Trivalent::Unknown,
                 Value::Integer(o) => (*s == Double::from(*o)).into(),
                 // If double can't be converted to decimal without error then it is not equal to decimal.
@@ -183,7 +274,7 @@ impl Value {
                 }
             },
             Value::Decimal(s) => match other {
-                Value::Boolean(_) | Value::String(_) => Trivalent::False,
+                Value::Boolean(_) | Value::String(_) | Value::Tuple(_) => Trivalent::False,
                 Value::Null => Trivalent::Unknown,
                 Value::Integer(o) => (s == &Decimal::from(*o)).into(),
                 Value::Decimal(o) => (s == o).into(),
@@ -192,7 +283,7 @@ impl Value {
                 Value::Unsigned(o) => (s == &Decimal::from(*o)).into(),
             },
             Value::Unsigned(s) => match other {
-                Value::Boolean(_) | Value::String(_) => Trivalent::False,
+                Value::Boolean(_) | Value::String(_) | Value::Tuple(_) => Trivalent::False,
                 Value::Null => Trivalent::Unknown,
                 Value::Integer(o) => (Decimal::from(*s) == *o).into(),
                 Value::Decimal(o) => (&Decimal::from(*s) == o).into(),
@@ -207,10 +298,21 @@ impl Value {
                 | Value::Integer(_)
                 | Value::Decimal(_)
                 | Value::Double(_)
-                | Value::Unsigned(_) => Trivalent::False,
+                | Value::Unsigned(_)
+                | Value::Tuple(_) => Trivalent::False,
                 Value::Null => Trivalent::Unknown,
                 Value::String(o) => s.eq(o).into(),
             },
+            Value::Tuple(_) => match other {
+                Value::Boolean(_)
+                | Value::Integer(_)
+                | Value::Decimal(_)
+                | Value::Double(_)
+                | Value::Unsigned(_)
+                | Value::String(_)
+                | Value::Tuple(_) => Trivalent::False,
+                Value::Null => Trivalent::Unknown,
+            },
         }
     }
 }
@@ -229,6 +331,7 @@ impl ToHashString for Value {
             Value::Double(v) => v.to_string(),
             Value::Boolean(v) => v.to_string(),
             Value::String(v) => v.to_string(),
+            Value::Tuple(v) => v.to_string(),
             Value::Null => "NULL".to_string(),
         }
     }
@@ -246,6 +349,7 @@ impl Serialize for Value {
             Value::Double(Double { value }) => serializer.serialize_f64(*value),
             Value::Boolean(v) => serializer.serialize_bool(*v),
             Value::String(v) => serializer.serialize_str(v),
+            Value::Tuple(v) => v.serialize(serializer),
             Value::Null => serializer.serialize_none(),
         }
     }
@@ -266,6 +370,7 @@ impl<'de> Deserialize<'de> for Value {
             Float(f64),
             Boolean(bool),
             String(String),
+            Tuple(Tuple),
             Null(()),
         }
 
@@ -289,6 +394,7 @@ impl<'de> Deserialize<'de> for Value {
                     }
                     EncodedValue::Boolean(v) => Value::Boolean(v),
                     EncodedValue::String(v) => Value::String(v),
+                    EncodedValue::Tuple(v) => Value::Tuple(v),
                     EncodedValue::Null(_) => Value::Null,
                 }
             }
@@ -308,6 +414,7 @@ impl From<Value> for String {
             Value::Double(v) => v.to_string(),
             Value::Boolean(v) => v.to_string(),
             Value::String(v) => v,
+            Value::Tuple(v) => v.to_string(),
             Value::Null => "NULL".to_string(),
         }
     }
@@ -324,6 +431,7 @@ impl<L: tlua::AsLua> tlua::Push<L> for Value {
             Value::Double(v) => v.push_to_lua(lua),
             Value::Boolean(v) => v.push_to_lua(lua),
             Value::String(v) => v.push_to_lua(lua),
+            Value::Tuple(v) => v.push_to_lua(lua),
             Value::Null => tlua::Null.push_to_lua(lua),
         }
     }
@@ -334,7 +442,8 @@ where
     L: tlua::AsLua,
 {
     type Err = tlua::Void;
-    fn push_into_lua(self, lua: L) -> Result<tlua::PushGuard<L>, (tlua::Void, L)> {
+
+    fn push_into_lua(self, lua: L) -> Result<tlua::PushGuard<L>, (Self::Err, L)> {
         match self {
             Value::Unsigned(v) => v.push_into_lua(lua),
             Value::Integer(v) => v.push_into_lua(lua),
@@ -342,6 +451,7 @@ where
             Value::Double(v) => v.push_into_lua(lua),
             Value::Boolean(v) => v.push_into_lua(lua),
             Value::String(v) => v.push_into_lua(lua),
+            Value::Tuple(v) => v.push_into_lua(lua),
             Value::Null => tlua::Null.push_into_lua(lua),
         }
     }
@@ -395,6 +505,10 @@ where
             Ok(v) => return Ok(Self::String(v)),
             Err(lua) => lua,
         };
+        let lua = match tlua::LuaRead::lua_read_at_position(lua, index) {
+            Ok(v) => return Ok(Self::Tuple(v)),
+            Err(lua) => lua,
+        };
         let lua = match tlua::Null::lua_read_at_position(lua, index) {
             Ok(_) => return Ok(Self::Null),
             Err(lua) => lua,
diff --git a/sbroad-core/src/ir/value/tests.rs b/sbroad-core/src/ir/value/tests.rs
index b2d7cd9d5e0373aaa258a1cf5a5f4787b4f5461d..5032ee2e33058fc1ff428c8902198b01b979a7fd 100644
--- a/sbroad-core/src/ir/value/tests.rs
+++ b/sbroad-core/src/ir/value/tests.rs
@@ -92,6 +92,15 @@ fn unsigned() {
     assert_ne!(Value::Unsigned(0_u64), Value::from(1_u64));
 }
 
+#[test]
+fn tuple() {
+    let t = Tuple::from(vec![Value::Unsigned(0), Value::String("hello".to_string())]);
+    assert_eq!(
+        Value::Tuple(t),
+        Value::from(vec![Value::from(0_u64), Value::from("hello")])
+    );
+}
+
 #[test]
 #[allow(clippy::too_many_lines)]
 fn equivalence() {