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() {