diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..5617d5bd634213fb61658a66e82ea8210b0695fb --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +[target.aarch64-apple-darwin] +rustflags = [ + "-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup" +] diff --git a/Cargo.toml b/Cargo.toml index 1282ef2f9d384a543fcfcd4ef20b7c9c2049e701..a2f5f82156b6c4324574d69cbfca9da24bc0f8cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ edition = "2021" [dependencies] decimal = "2.1.0" -fasthash = "0.4.0" +hash32 = "0.2" itertools = "0.10.3" pest = "2.0" pest_derive = "2.0" @@ -20,10 +20,10 @@ serde_yaml = "0.8" tarantool = { git = "https://sbroad-cargo-token:t-nZyqJVVuhGQv17BX6v@gitlab.com/picodata/picodata/tarantool-module.git", rev="d8921ec6"} traversal = "0.1.2" yaml-rust = "0.4.1" +rmp-serde = "0.14" [dev-dependencies] pretty_assertions = "1.0.0" -rmp-serde = "0.14" [lib] crate-type = ["cdylib"] diff --git a/src/executor.rs b/src/executor.rs index 078d8b4d736153b4b3752dc9da9466c1ea1432c5..cc6bcbf6df901590ed1d50ba92ece0d51832dc23 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -36,7 +36,7 @@ use std::collections::HashMap; mod bucket; pub mod engine; pub(crate) mod ir; -mod result; +pub mod result; mod vtable; impl Plan { diff --git a/src/executor/engine.rs b/src/executor/engine.rs index da1dfa7fdcf4187e112ad29fe8463c2d2c415f53..dc26e9b04643c4becccf1021727eca450d86be1e 100644 --- a/src/executor/engine.rs +++ b/src/executor/engine.rs @@ -2,11 +2,14 @@ //! //! Traits that define an execution engine interface. +use std::collections::HashMap; + use crate::errors::QueryPlannerError; use crate::executor::bucket::Buckets; use crate::executor::ir::ExecutionPlan; use crate::executor::result::BoxExecuteFormat; use crate::executor::vtable::VirtualTable; +use crate::ir::value::Value as IrValue; pub mod cartridge; @@ -33,6 +36,13 @@ pub trait Metadata { format!("\"{}\"", s) } } + + /// Provides an `Vec<&str>` with sharding keys or an error + /// + /// # Errors + /// - Metadata does not contains space + /// - Metadata contains incorrect sharding keys format + fn get_sharding_key_by_space(&self, space: &str) -> Result<Vec<&str>, QueryPlannerError>; } /// An execution engine trait. @@ -78,6 +88,17 @@ pub trait Engine { buckets: &Buckets, ) -> Result<BoxExecuteFormat, QueryPlannerError>; + /// Filter lua table values and return in right order + /// + /// # Errors + /// - args does not contains all sharding keys + /// - internal metadata errors + fn extract_sharding_keys( + &self, + space: String, + args: HashMap<String, IrValue>, + ) -> Result<Vec<IrValue>, QueryPlannerError>; + /// Determine shard for query execution by sharding key value fn determine_bucket_id(&self, s: &str) -> u64; } diff --git a/src/executor/engine/cartridge.rs b/src/executor/engine/cartridge.rs index d1339467b2151a63839e130121454a36a5b24e07..633d055e7dabcd18c137133f29e927f78ef4f164 100644 --- a/src/executor/engine/cartridge.rs +++ b/src/executor/engine/cartridge.rs @@ -1,5 +1,6 @@ //! Tarantool cartridge engine module. +use std::collections::HashMap; use std::convert::TryInto; use tarantool::log::{say, SayLevel}; @@ -14,6 +15,7 @@ use crate::executor::ir::ExecutionPlan; use crate::executor::result::BoxExecuteFormat; use crate::executor::vtable::VirtualTable; use crate::executor::Metadata; +use crate::ir::value::Value as IrValue; mod backend; pub mod cache; @@ -155,6 +157,31 @@ impl Engine for Runtime { Ok(vtable) } + /// Extract from the `HashMap<String, Value>` only those values that + /// correspond to the fields of the sharding key. + /// Returns the values of sharding keys in ordered form + fn extract_sharding_keys( + &self, + space: String, + rec: HashMap<String, IrValue>, + ) -> Result<Vec<IrValue>, QueryPlannerError> { + self.metadata() + .get_sharding_key_by_space(space.as_str())? + .iter() + .try_fold(Vec::new(), |mut acc: Vec<IrValue>, &val| { + match rec.get(val) { + Some(value) => { + acc.push(value.clone()); + Ok(acc) + } + None => Err(QueryPlannerError::CustomError(format!( + "The dict of args missed key/value to calculate bucket_id. Column: {}", + val + ))), + } + }) + } + /// Calculate bucket for a key. fn determine_bucket_id(&self, s: &str) -> u64 { str_to_bucket_id(s, self.bucket_count) diff --git a/src/executor/engine/cartridge/cache.rs b/src/executor/engine/cartridge/cache.rs index 2e30d4caaa4c3e845fc8529aac1aec83a3d7d1f1..02547089e86e9b7363399d9300bd70f3c1027cef 100644 --- a/src/executor/engine/cartridge/cache.rs +++ b/src/executor/engine/cartridge/cache.rs @@ -60,27 +60,6 @@ impl ClusterAppConfig { Err(QueryPlannerError::InvalidClusterSchema) } - /// Get table sharding key. - /// - /// # Panics - /// - Invalid schema. - #[allow(dead_code)] - #[must_use] - pub fn get_sharding_key_by_space(self, space: &str) -> Vec<String> { - let mut result = Vec::new(); - let spaces = self.schema["spaces"].as_hash().unwrap(); - - for (space_name, params) in spaces.iter() { - let current_space_name = space_name.as_str().unwrap(); - if current_space_name == space { - for k in params["sharding_key"].as_vec().unwrap() { - result.push(Self::to_name(k.as_str().unwrap())); - } - } - } - result - } - pub(in crate::executor::engine::cartridge) fn is_empty(&self) -> bool { self.schema.is_null() } @@ -169,6 +148,28 @@ impl Metadata for ClusterAppConfig { fn get_exec_waiting_timeout(&self) -> u64 { self.waiting_timeout } + + /// Get sharding keys by space name + fn get_sharding_key_by_space(&self, space: &str) -> Result<Vec<&str>, QueryPlannerError> { + if let Some(vec) = self.schema["spaces"][space]["sharding_key"].as_vec() { + return vec + .iter() + .try_fold(Vec::new(), |mut acc: Vec<&str>, str| match str.as_str() { + Some(val) => { + acc.push(val); + Ok(acc) + } + _ => Err(QueryPlannerError::CustomError(format!( + "Schema {} contains incorrect sharding keys format", + space + ))), + }); + } + Err(QueryPlannerError::CustomError(format!( + "Failed to get space {} or sharding key", + space + ))) + } } #[cfg(test)] diff --git a/src/executor/engine/cartridge/cache/tests.rs b/src/executor/engine/cartridge/cache/tests.rs index 901eb527bfe7135827eb398fda597ff01d8051fd..1451c68136d000638f890401a09c46d66930a81b 100644 --- a/src/executor/engine/cartridge/cache/tests.rs +++ b/src/executor/engine/cartridge/cache/tests.rs @@ -85,12 +85,10 @@ fn test_yaml_schema_parser() { let mut s = ClusterAppConfig::new(); s.load_schema(test_schema).unwrap(); - let mut expected_keys = Vec::new(); - expected_keys.push("\"identification_number\"".to_string()); - expected_keys.push("\"product_code\"".to_string()); + let expected_keys = vec!["identification_number", "product_code"]; // FIXME: do we need "to_name()" here? - let actual_keys = s.get_sharding_key_by_space("hash_testing"); + let actual_keys = s.get_sharding_key_by_space("hash_testing").unwrap(); assert_eq!(actual_keys, expected_keys) } diff --git a/src/executor/engine/cartridge/hash.rs b/src/executor/engine/cartridge/hash.rs index 5cf31af77a05b6d84f18e461dc386c240179158c..1552289c084d4a87a0182efe91a792558ae3f782 100644 --- a/src/executor/engine/cartridge/hash.rs +++ b/src/executor/engine/cartridge/hash.rs @@ -1,13 +1,14 @@ //! Bucket hash module. -use fasthash::{murmur3::Hasher32, FastHasher}; -use std::hash::Hasher; +use hash32::{Hasher, Murmur3Hasher}; -/// Determine bucket value using `murmur3` hash function -pub(in crate::executor::engine) fn str_to_bucket_id(s: &str, bucket_count: usize) -> u64 { - let mut hash = Hasher32::new(); - hash.write(s.as_bytes()); - hash.finish() % bucket_count as u64 + 1 +#[must_use] +/// A simple function to calculate the bucket id from a string slice. +/// `(MurMur3 hash at str) % bucket_count + 1` +pub fn str_to_bucket_id(s: &str, bucket_count: usize) -> u64 { + let mut hasher = Murmur3Hasher::default(); + hasher.write(s.as_bytes()); + u64::from(hasher.finish()) % bucket_count as u64 + 1 } #[cfg(test)] diff --git a/src/executor/engine/cartridge/hash/tests.rs b/src/executor/engine/cartridge/hash/tests.rs index b7c50c2cb3489756305c68b9dea9b61c071a1dc9..29f138aa22200ec5bf6249135bc6da64b12ca78b 100644 --- a/src/executor/engine/cartridge/hash/tests.rs +++ b/src/executor/engine/cartridge/hash/tests.rs @@ -4,12 +4,15 @@ use super::*; fn test_bucket_id_by_str() { assert_eq!(str_to_bucket_id("100теÑÑ‚Ñ‹", 30000), 17339); - assert_eq!( - str_to_bucket_id("4TEST5501605647472000000100000000d92beee8-749f-4539-aa15-3d2941dbb0f1x32https://google.com", 30000), - 13815 + assert_eq!(str_to_bucket_id( + "4TEST5501605647472000000100000000d9\ + 2beee8-749f-4539-aa15-3d2941dbb0f1x32https://google.com", 30000), + 13815 ); - assert_eq!(360, str_to_bucket_id("1123", 30000),); + assert_eq!(str_to_bucket_id("1123", 30000), 360); + + assert_eq!(str_to_bucket_id("daymonthyear", 30000), 3333); } #[test] diff --git a/src/executor/engine/mock.rs b/src/executor/engine/mock.rs index 0c9ec71d2c8afe680b1bd09695e965d1705f3737..8092b16f340aae21f405613ef2416aeb482be58b 100644 --- a/src/executor/engine/mock.rs +++ b/src/executor/engine/mock.rs @@ -9,10 +9,12 @@ use crate::executor::result::{BoxExecuteFormat, Value}; use crate::executor::vtable::VirtualTable; use crate::executor::Metadata; use crate::ir::relation::{Column, Table, Type}; +use crate::ir::value::Value as IrValue; #[allow(clippy::module_name_repetitions)] #[derive(Debug, Clone)] pub struct MetadataMock { + schema: HashMap<String, Vec<String>>, tables: HashMap<String, Table>, bucket_count: usize, } @@ -29,6 +31,14 @@ impl Metadata for MetadataMock { fn get_exec_waiting_timeout(&self) -> u64 { 0 } + + fn get_sharding_key_by_space(&self, space: &str) -> Result<Vec<&str>, QueryPlannerError> { + Ok(self + .schema + .get(space) + .map(|v| v.iter().map(|s| s.as_str()).collect::<Vec<&str>>()) + .unwrap()) + } } impl MetadataMock { @@ -103,6 +113,15 @@ impl MetadataMock { ); MetadataMock { + schema: [ + ("EMPLOYEES".into(), vec!["ID".into()]), + ( + "hash_testing".into(), + vec!["identification_number".into(), "product_code".into()], + ), + ] + .into_iter() + .collect(), tables, bucket_count: 10000, } @@ -180,6 +199,22 @@ impl Engine for EngineMock { Ok(result) } + fn extract_sharding_keys( + &self, + space: String, + args: HashMap<String, IrValue>, + ) -> Result<Vec<IrValue>, QueryPlannerError> { + Ok(self + .metadata() + .get_sharding_key_by_space(&space) + .unwrap() + .iter() + .fold(Vec::new(), |mut acc: Vec<IrValue>, &v| { + acc.push(args.get(v).unwrap().clone()); + acc + })) + } + fn determine_bucket_id(&self, s: &str) -> u64 { str_to_bucket_id(s, self.metadata.bucket_count) } diff --git a/src/executor/result.rs b/src/executor/result.rs index 0e0fc1a720f530dbdc42679f9df01d978f3fd25f..82fffab1e2eae1eae463546f074428272f6ce8c3 100644 --- a/src/executor/result.rs +++ b/src/executor/result.rs @@ -1,7 +1,9 @@ use std::fmt; use decimal::d128; +use serde::de::Visitor; use serde::ser::{Serialize, SerializeMap, Serializer}; +use serde::Deserialize; use tarantool::tlua::{self, LuaRead}; use crate::errors::QueryPlannerError; @@ -65,6 +67,76 @@ impl Serialize for Value { } } +struct ValueVistor; + +impl<'de> Visitor<'de> for ValueVistor { + type Value = Value; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a tarantool value enum implementation") + } + + fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(Value::Boolean(value)) + } + fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(Value::Integer(value)) + } + + fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(Value::Unsigned(value)) + } + + fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(Value::Number(value)) + } + + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(Value::String(v.to_string())) + } + + fn visit_string<E>(self, value: String) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(Value::String(value)) + } + + fn visit_unit<E>(self) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(Value::Null(tlua::Null)) + } +} + +/// Custom Implementation `de::Deserialize`, because if using standard `#derive[Deserialize]` +/// then each `Value` type record deserialize as Value +/// and it doesn't correct for result format +impl<'de> Deserialize<'de> for Value { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(ValueVistor) + } +} + impl Eq for Value {} type BoxExecuteTuple = Vec<Value>; diff --git a/src/ir/value.rs b/src/ir/value.rs index c85679e47662468f379e6bcac9295687aae28be3..f85761f6656292a292beb656d3aef974f90eb069 100644 --- a/src/ir/value.rs +++ b/src/ir/value.rs @@ -51,6 +51,29 @@ pub enum Value { String(String), } +impl From<crate::executor::result::Value> for Value { + fn from(value: crate::executor::result::Value) -> Self { + match value { + crate::executor::result::Value::Boolean(v) => Value::Boolean(v), + // Here is absolutely stupid solution! + // d128 supports floating point values, and f64 conversion cannot cause errors. + // But in crate d128 there is no implementation of `From<f64>`, and the development + // of the crate is dead. + // We have only one way to convert f64 to d128 is a `FromStr` trait, which is also + // not implemented idiomatically. Realization despite the signature with a potential + // error, but always returns a value. For this reason, this trait does not implement + // a Error, since we can be sure that all checks were passed when f64 was received. + crate::executor::result::Value::Number(v) => Value::Number( + d128::from_str(&v.to_string()).expect("Invalid decimal float literal"), + ), + crate::executor::result::Value::Integer(v) => Value::Number(d128::from(v)), + crate::executor::result::Value::String(v) => Value::String(v), + crate::executor::result::Value::Unsigned(v) => Value::Number(d128::from(v)), + crate::executor::result::Value::Null(_) => Value::Null, + } + } +} + impl Eq for Value {} impl fmt::Display for Value { diff --git a/src/parser.rs b/src/parser.rs index 12041bbc0188003743248bfebb1c983190b5bb31..563ba05a72e214bf7e30e4ad6e2c8a7a15a75930 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,3 +1,5 @@ +mod extargs; + use std::cell::RefCell; use std::convert::TryInto; use std::os::raw::c_int; @@ -7,9 +9,12 @@ use tarantool::error::TarantoolErrorCode; use tarantool::log::{say, SayLevel}; use tarantool::tuple::{AsTuple, FunctionArgs, FunctionCtx, Tuple}; +use crate::errors::QueryPlannerError; use crate::executor::engine::{cartridge, Engine}; use crate::executor::Query; +use self::extargs::{BucketCalcArgs, BucketCalcArgsDict}; + thread_local!(static QUERY_ENGINE: RefCell<cartridge::Runtime> = RefCell::new(cartridge::Runtime::new().unwrap())); #[derive(Serialize, Deserialize)] @@ -32,25 +37,58 @@ pub extern "C" fn invalidate_caching_schema(ctx: FunctionCtx, _: FunctionArgs) - 0 } -#[derive(Serialize, Deserialize)] -struct BucketCalcArgs { - pub val: String, -} - -impl AsTuple for BucketCalcArgs {} - #[no_mangle] pub extern "C" fn calculate_bucket_id(ctx: FunctionCtx, args: FunctionArgs) -> c_int { let args: Tuple = args.into(); let args = args.into_struct::<BucketCalcArgs>().unwrap(); QUERY_ENGINE.with(|e| { - let result = e.clone().into_inner().determine_bucket_id(&args.val); + let result = e.clone().into_inner().determine_bucket_id(&args.rec); ctx.return_mp(&result).unwrap(); 0 }) } +#[no_mangle] +pub extern "C" fn calculate_bucket_id_by_dict(ctx: FunctionCtx, args: FunctionArgs) -> c_int { + QUERY_ENGINE.with(|e| { + let mut engine = e.clone().into_inner(); + // Update cartridge schema after cache invalidation by calling `apply_config()` in lua code. + if engine.has_metadata() { + match engine.load_metadata() { + Ok(_) => *e.borrow_mut() = engine.clone(), + Err(e) => { + return tarantool::set_error!(TarantoolErrorCode::ProcC, "{}", e.to_string()); + } + }; + } + + // Closure for more concise error propagation from calls nested in the bucket calculation + let propagate_err = || -> Result<u64, QueryPlannerError> { + // Deserialization error + let bca = BucketCalcArgsDict::try_from(args)?; + // Error in filtering bucket calculation arguments by sharding keys + let fk = engine + .extract_sharding_keys(bca.space, bca.rec)? + .into_iter() + .fold(String::new(), |mut acc, v| { + let s: String = v.into(); + acc.push_str(s.as_str()); + acc + }); + Ok(e.clone().into_inner().determine_bucket_id(fk.as_str())) + }; + + match propagate_err() { + Ok(bucket_id) => { + ctx.return_mp(&bucket_id).unwrap(); + 0 + } + Err(e) => tarantool::set_error!(TarantoolErrorCode::ProcC, "{:?}", e), + } + }) +} + #[derive(Debug, Serialize, Deserialize)] struct ExecQueryArgs { pub query: String, diff --git a/src/parser/extargs.rs b/src/parser/extargs.rs new file mode 100644 index 0000000000000000000000000000000000000000..9dc0281ce4c83561a7e745f3ad354b757dfdc11a --- /dev/null +++ b/src/parser/extargs.rs @@ -0,0 +1,71 @@ +use std::{collections::HashMap, convert::TryFrom, ops::Deref}; + +use serde::{de::Deserializer, Deserialize, Serialize}; +use tarantool::tuple::{AsTuple, FunctionArgs, Tuple}; + +use crate::{errors::QueryPlannerError, executor::result::Value, ir::value::Value as IrValue}; + +#[derive(Serialize, Deserialize)] +pub struct BucketCalcArgs { + pub rec: String, +} + +impl AsTuple for BucketCalcArgs {} + +#[derive(Debug, Default, Serialize, PartialEq)] +/// Tuple with space name and `key:value` map of values +pub struct BucketCalcArgsDict { + /// Space name as `String` + pub space: String, + /// A key:value `HashMap` with key String and custom type IrValue + pub rec: HashMap<String, IrValue>, +} + +impl Deref for BucketCalcArgsDict { + type Target = HashMap<String, IrValue>; + + fn deref(&self) -> &Self::Target { + &self.rec + } +} + +impl TryFrom<FunctionArgs> for BucketCalcArgsDict { + type Error = QueryPlannerError; + + fn try_from(value: FunctionArgs) -> Result<Self, Self::Error> { + Tuple::from(value) + .into_struct::<BucketCalcArgsDict>() + .map_err(|e| { + QueryPlannerError::CustomError(format!( + "Error then deserializing tuple into BucketCalcArgsDict! {:?}", + e + )) + }) + } +} + +impl<'de> Deserialize<'de> for BucketCalcArgsDict { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(rename = "BucketCalcArgsDict")] + struct StructHelper(String, HashMap<String, Value>); + + let struct_helper = StructHelper::deserialize(deserializer)?; + Ok(BucketCalcArgsDict { + space: struct_helper.0, + rec: struct_helper + .1 + .into_iter() + .map(|(k, v)| (k, IrValue::from(v))) + .collect(), + }) + } +} + +impl AsTuple for BucketCalcArgsDict {} + +#[cfg(test)] +mod tests; diff --git a/src/parser/extargs/tests.rs b/src/parser/extargs/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..244913695fa0a2a256f776ecb6f3b40a98418d21 --- /dev/null +++ b/src/parser/extargs/tests.rs @@ -0,0 +1,59 @@ +use decimal::d128; + +use crate::{ + executor::engine::{mock::EngineMock, Engine}, + ir::value::Value as IrValue, + parser::extargs::BucketCalcArgsDict, +}; + +#[test] +fn bucket_calc_args() { + let engine = EngineMock::new(); + let args = BucketCalcArgsDict { + space: "EMPLOYEES".into(), + rec: [(String::from("ID"), IrValue::Number(d128!(100.0)))] + .iter() + .cloned() + .collect(), + }; + let filtered_args = engine + .extract_sharding_keys(args.space, args.rec) + .unwrap() + .into_iter() + .fold(String::new(), |mut acc, v| { + let s: String = v.into(); + acc.push_str(s.as_str()); + acc + }); + + assert_eq!(engine.determine_bucket_id(&filtered_args), 2377); + + let args = BucketCalcArgsDict { + space: "hash_testing".into(), + rec: [ + ( + String::from("identification_number"), + IrValue::Number(d128!(93312)), + ), + ( + String::from("product_code"), + IrValue::String("fff100af".into()), + ), + (String::from("product_units"), IrValue::Number(d128!(10))), + (String::from("sys_op"), IrValue::Number(d128!(981.945))), + ] + .iter() + .cloned() + .collect(), + }; + let filtered_args = engine + .extract_sharding_keys(args.space, args.rec) + .unwrap() + .into_iter() + .fold(String::new(), |mut acc, v| { + let s: String = v.into(); + acc.push_str(s.as_str()); + acc + }); + assert_eq!(engine.determine_bucket_id(&filtered_args), 7704); +} diff --git a/test_app/app/roles/api.lua b/test_app/app/roles/api.lua index d32c823f036b183ffa7008592e064e6fa6a857b6..24357557115da6ef209a22b88eeee82e22d8e54b 100644 --- a/test_app/app/roles/api.lua +++ b/test_app/app/roles/api.lua @@ -2,9 +2,12 @@ local vshard = require('vshard') local cartridge = require('cartridge') _G.query = nil +_G.calculate_bucket_id = nil +_G.calculate_bucket_id_by_dict = nil _G.insert_record = nil _G.sql_execute = nil + local function query(q) local has_err, parser_res = pcall( function() @@ -19,7 +22,8 @@ local function query(q) return parser_res end -local function insert_record(space_name, values) +local function calculate_bucket_id(space_name, values) + checks('string', 'table') local shard_key = cartridge.config_get_deepcopy().schema.spaces[space_name].sharding_key local shard_val = '' @@ -27,7 +31,28 @@ local function insert_record(space_name, values) shard_val = shard_val .. tostring(values[key]) end - values['bucket_id'] = box.func["sbroad.calculate_bucket_id"]:call({ shard_val }) + local bucket_id = box.func["sbroad.calculate_bucket_id"]:call({ shard_val }) + return bucket_id +end + +local function calculate_bucket_id_by_dict(space_name, values) -- luacheck: no unused args + checks('string', 'table') + + local has_err, calc_err = pcall( + function() + return box.func["sbroad.calculate_bucket_id_by_dict"]:call({ space_name, values }) + end + ) + + if has_err == false then + return nil, calc_err + end + + return calc_err +end + +local function insert_record(space_name, values) + values['bucket_id'] = calculate_bucket_id(space_name, values) local res = vshard.router.call( values['bucket_id'], "write", @@ -41,13 +66,27 @@ local function init(opts) -- luacheck: no unused args -- if opts.is_master then -- end _G.query = query + _G.calculate_bucket_id = calculate_bucket_id + _G.calculate_bucket_id_by_dict = calculate_bucket_id_by_dict _G.insert_record = insert_record _G.sql_execute = sql_execute - box.schema.func.create('sbroad.parse_sql', { if_not_exists = true, language = 'C' }) - box.schema.func.create('sbroad.invalidate_caching_schema', { if_not_exists = true, language = 'C' }) - box.schema.func.create('sbroad.calculate_bucket_id', { if_not_exists = true, language = 'C' }) - box.schema.func.create('sbroad.execute_query', { if_not_exists = true, language = 'C' }) + + box.schema.func.create('sbroad.parse_sql', { + if_not_exists = true, language = 'C' + }) + box.schema.func.create('sbroad.invalidate_caching_schema', { + if_not_exists = true, language = 'C' + }) + box.schema.func.create('sbroad.calculate_bucket_id', { + if_not_exists = true, language = 'C' + }) + box.schema.func.create('sbroad.calculate_bucket_id_by_dict', { + if_not_exists = true, language = 'C' + }) + box.schema.func.create('sbroad.execute_query', { + if_not_exists = true, language = 'C' + }) return true end diff --git a/test_app/test/integration/api_test.lua b/test_app/test/integration/api_test.lua index c197ab5f18b936cdf05d8a9477a8aa5954174003..c0a333589d173f2feb75a26fb0313811daa3d23a 100644 --- a/test_app/test/integration/api_test.lua +++ b/test_app/test/integration/api_test.lua @@ -43,6 +43,21 @@ g.after_each( end ) +g.test_bucket_id_calculation = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("calculate_bucket_id", { "testing_space", { id = 1, name = "123", product_units = 1 } }) + t.assert_equals(err, nil) + t.assert_equals(r, 360) + + r, err = api:call("calculate_bucket_id_by_dict", { "testing_space", { id = 1, name = "123", product_units = 1 } }) + t.assert_equals(err, nil) + t.assert_equals(r, 360) + + _, err = api:call("calculate_bucket_id_by_dict", { "testing_space", { id = 1 }}) + t.assert_equals(err, "CustomError(\"The dict of args missed key/value to calculate bucket_id. Column: name\")") +end + g.test_incorrect_query = function() local api = cluster:server("api-1").net_box @@ -332,4 +347,4 @@ g.test_empty_motion_result = function() {1, "123", 1, 360} }, }) -end \ No newline at end of file +end diff --git a/test_app/test_app-scm-1.rockspec b/test_app/test_app-scm-1.rockspec index fe2414906ee7e66cdad0f0a9bf08e22b595e284f..7cc9972857e5ad5c43d147c2458abcd192eb2085 100644 --- a/test_app/test_app-scm-1.rockspec +++ b/test_app/test_app-scm-1.rockspec @@ -8,8 +8,8 @@ dependencies = { 'tarantool', 'lua >= 5.1', 'checks == 3.1.0-1', - 'cartridge == 2.6.0-1', - 'metrics == 0.8.0-1', + 'cartridge == 2.7.3-1', + 'metrics == 0.12.0-1', 'cartridge-cli-extensions == 1.1.1-1', 'luatest', 'luacov'