diff --git a/CHANGELOG.md b/CHANGELOG.md index 11de79d7..fc1a9485 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. * Option flag `force_map_call` for `select()`/`pairs()` to disable the `bucket_id` computation from primary key. * `crud.min` and `crud.max` functions to find the minimum and maximum values in the specified index. +* Added support for jsonpath for select. ## [0.6.0] - 2021-03-29 diff --git a/crud/compare/conditions.lua b/crud/compare/conditions.lua index 17396b6c..3d963650 100644 --- a/crud/compare/conditions.lua +++ b/crud/compare/conditions.lua @@ -63,7 +63,7 @@ for func_name, operator in pairs(cond_operators_by_func_names) do return new_condition({ operator = operator, operand = operand, - values = values + values = values, }) end end diff --git a/crud/select/filters.lua b/crud/select/filters.lua index a11e42ce..6cf502b6 100644 --- a/crud/select/filters.lua +++ b/crud/select/filters.lua @@ -1,4 +1,3 @@ -local json = require('json') local errors = require('errors') local utils = require('crud.common.utils') @@ -6,7 +5,6 @@ local dev_checks = require('crud.common.dev_checks') local collations = require('crud.common.collations') local compare_conditions = require('crud.compare.conditions') -local ParseConditionsError = errors.new_class('ParseConditionsError', {capture_stack = false}) local GenFiltersError = errors.new_class('GenFiltersError', {capture_stack = false}) local filters = {} @@ -97,31 +95,42 @@ local function parse(space, conditions, opts) for i, condition in ipairs(conditions) do if i ~= opts.scan_condition_num then -- Index check (including one and multicolumn) - local fieldnos - local fields_types + local fields + local fields_types = {} local values_opts local index = space_indexes[condition.operand] if index ~= nil then - fieldnos = get_index_fieldnos(index) + fields = get_index_fieldnos(index) fields_types = get_index_fields_types(index) values_opts = get_values_opts(index) - elseif fieldnos_by_names[condition.operand] ~= nil then - local fiendno = fieldnos_by_names[condition.operand] - fieldnos = {fiendno} - local field_format = space_format[fiendno] - fields_types = {field_format.type} - local is_nullable = field_format.is_nullable == true + else + local fieldno = fieldnos_by_names[condition.operand] + + if fieldno ~= nil then + fields = {fieldno} + else + -- We assume this is jsonpath, so it is + -- not in fieldnos_by_name map. + fields = {condition.operand} + end + + local field_format = space_format[fieldno] + local is_nullable + + if field_format ~= nil then + fields_types = {field_format.type} + is_nullable = field_format.is_nullable == true + end + values_opts = { {is_nullable = is_nullable, collation = nil}, } - else - return nil, ParseConditionsError('No field or index is found for condition %s', json.encode(condition)) end table.insert(filter_conditions, { - fieldnos = fieldnos, + fields = fields, operator = condition.operator, values = condition.values, types = fields_types, @@ -156,12 +165,30 @@ end local PARSE_ARGS_TEMPLATE = 'local tuple = ...' local LIB_FUNC_HEADER_TEMPLATE = 'function M.%s(%s)' +local function format_path(path) + local path_type = type(path) + if path_type == 'number' then + return tostring(path) + elseif path_type == 'string' then + return ('%q'):format(path) + end + + assert(false, ('Unexpected format: %s'):format(path_type)) +end + local function concat_conditions(conditions, operator) return '(' .. table.concat(conditions, (' %s '):format(operator)) .. ')' end -local function get_field_variable_name(fieldno) - return string.format('field_%s', fieldno) +local function get_field_variable_name(field) + local field_type = type(field) + if field_type == 'number' then + field = tostring(field) + elseif field_type == 'string' then + field = string.gsub(field, '([().^$%[%]%+%-%*%?%%\'"])', '_') + end + + return string.format('field_%s', field) end local function get_eq_func_name(id) @@ -173,16 +200,17 @@ local function get_cmp_func_name(id) end local function gen_tuple_fields_def_code(filter_conditions) - -- get field numbers - local fieldnos_added = {} - local fieldnos = {} + -- get field names + local fields_added = {} + local fields = {} for _, cond in ipairs(filter_conditions) do for i = 1, #cond.values do - local fieldno = cond.fieldnos[i] - if not fieldnos_added[fieldno] then - table.insert(fieldnos, fieldno) - fieldnos_added[fieldno] = true + local field = cond.fields[i] + + if not fields_added[field] then + table.insert(fields, field) + fields_added[field] = true end end end @@ -190,21 +218,21 @@ local function gen_tuple_fields_def_code(filter_conditions) -- gen definitions for all used fields local fields_def_parts = {} - for _, fieldno in ipairs(fieldnos) do + for _, field in ipairs(fields) do table.insert(fields_def_parts, string.format( 'local %s = tuple[%s]', - get_field_variable_name(fieldno), fieldno + get_field_variable_name(field), format_path(field) )) end return table.concat(fields_def_parts, '\n') end -local function format_comp_with_value(fieldno, func_name, value) +local function format_comp_with_value(field, func_name, value) return string.format( '%s(%s, %s)', func_name, - get_field_variable_name(fieldno), + get_field_variable_name(field), format_value(value) ) end @@ -238,7 +266,7 @@ local function format_eq(cond) local values_opts = cond.values_opts or {} for j = 1, #cond.values do - local fieldno = cond.fieldnos[j] + local field = cond.fields[j] local value = cond.values[j] local value_type = cond.types[j] local value_opts = values_opts[j] or {} @@ -254,7 +282,7 @@ local function format_eq(cond) func_name = 'eq_uuid' end - table.insert(cond_strings, format_comp_with_value(fieldno, func_name, value)) + table.insert(cond_strings, format_comp_with_value(field, func_name, value)) end return cond_strings @@ -265,7 +293,7 @@ local function format_lt(cond) local values_opts = cond.values_opts or {} for j = 1, #cond.values do - local fieldno = cond.fieldnos[j] + local field = cond.fields[j] local value = cond.values[j] local value_type = cond.types[j] local value_opts = values_opts[j] or {} @@ -279,9 +307,10 @@ local function format_lt(cond) elseif value_type == 'uuid' then func_name = 'lt_uuid' end + func_name = add_strict_postfix(func_name, value_opts) - table.insert(cond_strings, format_comp_with_value(fieldno, func_name, value)) + table.insert(cond_strings, format_comp_with_value(field, func_name, value)) end return cond_strings @@ -366,10 +395,10 @@ local function gen_cmp_array_func_code(operator, func_name, cond, func_args_code return table.concat(func_code_lines, '\n') end -local function function_args_by_fieldnos(fieldnos) +local function function_args_by_field(fields) local arg_names = {} - for _, fieldno in ipairs(fieldnos) do - table.insert(arg_names, get_field_variable_name(fieldno)) + for _, field in ipairs(fields) do + table.insert(arg_names, get_field_variable_name(field)) end return table.concat(arg_names, ', ') end @@ -408,8 +437,8 @@ local function gen_filter_code(filter_conditions) table.insert(filter_code_parts, '') for i, cond in ipairs(filter_conditions) do - local args_fieldnos = { unpack(cond.fieldnos, 1, #cond.values) } - local func_args_code = function_args_by_fieldnos(args_fieldnos) + local args_fields = { unpack(cond.fields, 1, #cond.values) } + local func_args_code = function_args_by_field(args_fields) local library_func_name, library_func_code = gen_library_func(i, cond, func_args_code) table.insert(library_funcs_code_parts, library_func_code) diff --git a/crud/select/plan.lua b/crud/select/plan.lua index d529e681..f6dbf5c6 100644 --- a/crud/select/plan.lua +++ b/crud/select/plan.lua @@ -6,9 +6,7 @@ local dev_checks = require('crud.common.dev_checks') local select_plan = {} -local SelectPlanError = errors.new_class('SelectPlanError', {capture_stack = false}) local IndexTypeError = errors.new_class('IndexTypeError', {capture_stack = false}) -local ValidateConditionsError = errors.new_class('ValidateConditionsError', {capture_stack = false}) local FilterFieldsError = errors.new_class('FilterFieldsError', {capture_stack = false}) local function index_is_allowed(index) @@ -42,34 +40,6 @@ local function get_index_for_condition(space_indexes, space_format, condition) end end -local function validate_conditions(conditions, space_indexes, space_format) - local field_names = {} - for _, field_format in ipairs(space_format) do - field_names[field_format.name] = true - end - - local index_names = {} - - -- If we use # (not table.maxn), we may lose indexes, when user drop some indexes. - -- E.g: we have table with indexes id {1, 2, 3, nil, nil, 6}. - -- If we use #{1, 2, 3, nil, nil, 6} (== 3) we will lose index with id = 6. - -- See details: https://github.com/tarantool/crud/issues/103 - for i = 0, table.maxn(space_indexes) do - local index = space_indexes[i] - if index ~= nil then - index_names[index.name] = true - end - end - - for _, condition in ipairs(conditions) do - if index_names[condition.operand] == nil and field_names[condition.operand] == nil then - return false, ValidateConditionsError:new("No field or index %q found", condition.operand) - end - end - - return true -end - local function extract_sharding_key_from_scan_value(scan_value, scan_index, sharding_index) if #scan_value < #sharding_index.parts then return nil @@ -157,11 +127,6 @@ function select_plan.new(space, conditions, opts) local space_indexes = space.index local space_format = space:format() - local ok, err = validate_conditions(conditions, space_indexes, space_format) - if not ok then - return nil, SelectPlanError:new('Passed bad conditions: %s', err) - end - if conditions == nil then -- also cdata conditions = {} end diff --git a/test/entrypoint/srv_select.lua b/test/entrypoint/srv_select.lua index 15d08188..98081b5d 100755 --- a/test/entrypoint/srv_select.lua +++ b/test/entrypoint/srv_select.lua @@ -126,6 +126,31 @@ package.preload['customers-storage'] = function() unique = false, if_not_exists = true, }) + + local developers_space = box.schema.space.create('developers', { + format = { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'last_name', type = 'string'}, + {name = 'age', type = 'number'}, + {name = 'additional', type = 'any'}, + }, + if_not_exists = true, + engine = engine, + }) + + -- primary index + developers_space:create_index('id_index', { + parts = { 'id' }, + if_not_exists = true, + }) + + developers_space:create_index('bucket_id', { + parts = { 'bucket_id' }, + unique = false, + if_not_exists = true, + }) end, } end diff --git a/test/integration/select_test.lua b/test/integration/select_test.lua index a75ffb3c..1d243216 100644 --- a/test/integration/select_test.lua +++ b/test/integration/select_test.lua @@ -31,6 +31,7 @@ pgroup:set_after_all(function(g) helpers.stop_cluster(g.cluster) end) pgroup:set_before_each(function(g) helpers.truncate_space_on_cluster(g.cluster, 'customers') + helpers.truncate_space_on_cluster(g.cluster, 'developers') end) @@ -1199,3 +1200,57 @@ pgroup:add('test_select_force_map_call', function(g) table.sort(objects, function(obj1, obj2) return obj1.bucket_id < obj2.bucket_id end) t.assert_equals(objects, customers) end) + +pgroup:add('test_jsonpath', function(g) + helpers.insert_objects(g, 'developers', { + { + id = 1, name = "Alexey", last_name = "Smith", + age = 20, additional = { a = { b = 140 } }, + }, { + id = 2, name = "Sergey", last_name = "Choppa", + age = 21, additional = { a = { b = 120 } }, + }, { + id = 3, name = "Mikhail", last_name = "Crossman", + age = 42, additional = {}, + }, { + id = 4, name = "Pavel", last_name = "White", + age = 51, additional = { a = { b = 50 } }, + }, { + id = 5, name = "Tatyana", last_name = "May", + age = 17, additional = { a = 55 }, + }, + }) + + local result, err = g.cluster.main_server.net_box:call('crud.select', + {'developers', {{'>=', '[5]', 40}}, {fields = {'name', 'last_name'}}}) + t.assert_equals(err, nil) + + local objects = crud.unflatten_rows(result.rows, result.metadata) + local expected_objects = { + {id = 3, name = "Mikhail", last_name = "Crossman"}, + {id = 4, name = "Pavel", last_name = "White"}, + } + t.assert_equals(objects, expected_objects) + + local result, err = g.cluster.main_server.net_box:call('crud.select', + {'developers', {{'<', '["age"]', 21}}, {fields = {'name', 'last_name'}}}) + t.assert_equals(err, nil) + + local objects = crud.unflatten_rows(result.rows, result.metadata) + local expected_objects = { + {id = 1, name = "Alexey", last_name = "Smith"}, + {id = 5, name = "Tatyana", last_name = "May"}, + } + t.assert_equals(objects, expected_objects) + + local result, err = g.cluster.main_server.net_box:call('crud.select', + {'developers', {{'>=', '[6].a.b', 55}}, {fields = {'name', 'last_name'}}}) + t.assert_equals(err, nil) + + local objects = crud.unflatten_rows(result.rows, result.metadata) + local expected_objects = { + {id = 1, name = "Alexey", last_name = "Smith"}, + {id = 2, name = "Sergey", last_name = "Choppa"}, + } + t.assert_equals(objects, expected_objects) +end) diff --git a/test/integration/simple_operations_test.lua b/test/integration/simple_operations_test.lua index 21a49f70..052cecc3 100644 --- a/test/integration/simple_operations_test.lua +++ b/test/integration/simple_operations_test.lua @@ -1006,3 +1006,4 @@ pgroup:add('test_partial_result_bad_input', function(g) t.assert_equals(result, nil) t.assert_str_contains(err.err, 'Space format doesn\'t contain field named "lastname"') end) + diff --git a/test/integration/updated_shema_test.lua b/test/integration/updated_shema_test.lua index 035cc0d5..b4f57d60 100644 --- a/test/integration/updated_shema_test.lua +++ b/test/integration/updated_shema_test.lua @@ -594,14 +594,13 @@ pgroup:add('test_select_field_added', function(g) server.net_box:call('create_bucket_id_index') end) - -- unknown field error + -- unknown field (no results) local obj, err = g.cluster.main_server.net_box:call( 'crud.select', {'customers', {{'==', 'extra', 'EXTRRRRA'}}} ) - t.assert_equals(obj, nil) - t.assert_is_not(err, nil) - t.assert_str_contains(err.err, "No field or index \"extra\" found") + t.assert_equals(obj.rows, {}) + t.assert_equals(err, nil) -- add extra field helpers.call_on_servers(g.cluster, {'s1-master', 's2-master'}, function(server) diff --git a/test/unit/parse_conditions_test.lua b/test/unit/parse_conditions_test.lua index b82b8643..98fd6add 100644 --- a/test/unit/parse_conditions_test.lua +++ b/test/unit/parse_conditions_test.lua @@ -101,3 +101,25 @@ g.test_parse_errors = function() 'condition[2] should be string, got "number" (condition 2)' ) end + +g.test_jsonpath_parse = function() + local user_conditions = { + {'==', '[\'name\']', 'Alexey'}, + {'=', '["name"].a.b', 'Sergey'}, + {'<', '["year"]["field_1"][\'field_2\']', 2021}, + {'<=', '[2].a', {1, 2, 3}}, + {'>', '[2]', 'Jackson'}, + {'>=', '[\'year\'].a["f2"][\'f3\']', 2017}, + } + + local conditions, err = compare_conditions.parse(user_conditions) + t.assert(err == nil) + t.assert_equals(conditions, { + cond_funcs.eq('[\'name\']', 'Alexey'), + cond_funcs.eq('["name"].a.b', 'Sergey'), + cond_funcs.lt('["year"]["field_1"][\'field_2\']', 2021), + cond_funcs.le('[2].a', {1, 2, 3}), + cond_funcs.gt('[2]', 'Jackson'), + cond_funcs.ge('[\'year\'].a["f2"][\'f3\']', 2017), + }) +end diff --git a/test/unit/select_filters_test.lua b/test/unit/select_filters_test.lua index c788b0f1..6b4cc246 100644 --- a/test/unit/select_filters_test.lua +++ b/test/unit/select_filters_test.lua @@ -83,7 +83,7 @@ g.test_parse = function() -- age filter (early exit is possible) local age_filter_condition = filter_conditions[1] t.assert_type(age_filter_condition, 'table') - t.assert_equals(age_filter_condition.fieldnos, {5}) + t.assert_equals(age_filter_condition.fields, {5}) t.assert_equals(age_filter_condition.operator, compare_conditions.operators.LT) t.assert_equals(age_filter_condition.values, {40}) t.assert_equals(age_filter_condition.types, {'number'}) @@ -92,7 +92,7 @@ g.test_parse = function() -- full_name filter local full_name_filter_condition = filter_conditions[2] t.assert_type(full_name_filter_condition, 'table') - t.assert_equals(full_name_filter_condition.fieldnos, {3, 4}) + t.assert_equals(full_name_filter_condition.fields, {3, 4}) t.assert_equals(full_name_filter_condition.operator, compare_conditions.operators.EQ) t.assert_equals(full_name_filter_condition.values, {'Ivan', 'Ivanov'}) t.assert_equals(full_name_filter_condition.types, {'string', 'string'}) @@ -115,7 +115,7 @@ g.test_parse = function() -- has_a_car filter local has_a_car_filter_condition = filter_conditions[3] t.assert_type(has_a_car_filter_condition, 'table') - t.assert_equals(has_a_car_filter_condition.fieldnos, {7}) + t.assert_equals(has_a_car_filter_condition.fields, {7}) t.assert_equals(has_a_car_filter_condition.operator, compare_conditions.operators.EQ) t.assert_equals(has_a_car_filter_condition.values, {true}) t.assert_equals(has_a_car_filter_condition.types, {'boolean'}) @@ -130,7 +130,7 @@ end g.test_one_condition_number = function() local filter_conditions = { { - fieldnos = {1}, + fields = {1}, operator = compare_conditions.operators.EQ, values = {3}, types = {'number'}, @@ -167,7 +167,7 @@ end g.test_one_condition_boolean = function() local filter_conditions = { { - fieldnos = {1}, + fields = {1}, operator = compare_conditions.operators.EQ, values = {true}, types = {'boolean'}, @@ -207,7 +207,7 @@ end g.test_one_condition_string = function() local filter_conditions = { { - fieldnos = {2}, + fields = {2}, operator = compare_conditions.operators.GT, values = {'dddddddd'}, types = {'string'}, @@ -248,14 +248,14 @@ end g.test_two_conditions = function() local filter_conditions = { { - fieldnos = {1}, + fields = {1}, operator = compare_conditions.operators.EQ, values = {4}, types = {'number'}, early_exit_is_possible = true, }, { - fieldnos = {3}, + fields = {3}, operator = compare_conditions.operators.GE, values = {"dddddddd"}, types = {'string'}, @@ -304,7 +304,7 @@ end g.test_two_conditions_non_nullable = function() local filter_conditions = { { - fieldnos = {2, 3}, + fields = {2, 3}, operator = compare_conditions.operators.GE, values = {"test", 5}, types = {'string', 'number'}, @@ -312,17 +312,17 @@ g.test_two_conditions_non_nullable = function() values_opts = { {is_nullable = false}, {is_nullable = true}, - } + }, }, { - fieldnos = {1}, + fields = {1}, operator = compare_conditions.operators.LT, values = {3}, types = {'number'}, early_exit_is_possible = true, values_opts = { {is_nullable = false}, - } + }, }, } @@ -372,7 +372,7 @@ end g.test_one_condition_with_nil_value = function() local filter_conditions = { { - fieldnos = {2, 3}, + fields = {2, 3}, operator = compare_conditions.operators.GE, values = {"test"}, types = {'string', 'number'}, @@ -380,7 +380,7 @@ g.test_one_condition_with_nil_value = function() values_opts = { {is_nullable = false}, {is_nullable = true}, - } + }, }, } @@ -415,7 +415,7 @@ end g.test_unicode_collation = function() local filter_conditions = { { - fieldnos = {1, 2, 3, 4}, + fields = {1, 2, 3, 4}, operator = compare_conditions.operators.EQ, values = {'A', 'Á', 'Ä', 6}, types = {'string', 'string', 'string', 'number'}, @@ -424,7 +424,7 @@ g.test_unicode_collation = function() {collation='unicode'}, {collation='unicode_ci'}, {collation='unicode_ci'}, - } + }, }, } @@ -461,7 +461,7 @@ end g.test_binary_and_none_collation = function() local filter_conditions = { { - fieldnos = {1, 2, 3}, + fields = {1, 2, 3}, operator = compare_conditions.operators.EQ, values = {'A', 'B', 'C'}, types = {'string', 'string', 'string'}, @@ -470,7 +470,7 @@ g.test_binary_and_none_collation = function() {collation='none'}, {collation='binary'}, {collation=nil}, - } + }, }, } @@ -506,7 +506,7 @@ end g.test_null_as_last_value_eq = function() local filter_conditions = { { - fieldnos = {1, 2}, + fields = {1, 2}, operator = compare_conditions.operators.EQ, values = {'a', box.NULL}, types = {'string', 'string'}, @@ -514,7 +514,7 @@ g.test_null_as_last_value_eq = function() values_opts = { nil, {is_nullable = true}, - } + }, }, } @@ -547,7 +547,7 @@ end g.test_null_as_last_value_gt = function() local filter_conditions = { { - fieldnos = {1, 2}, + fields = {1, 2}, operator = compare_conditions.operators.GT, values = {'a', box.NULL}, types = {'string', 'string'}, @@ -555,7 +555,7 @@ g.test_null_as_last_value_gt = function() values_opts = { nil, {is_nullable = true}, - } + }, }, } @@ -594,7 +594,7 @@ end g.test_null_as_last_value_gt_non_nullable = function() local filter_conditions = { { - fieldnos = {1, 2}, + fields = {1, 2}, operator = compare_conditions.operators.GT, values = {'a', box.NULL}, types = {'string', 'string'}, @@ -602,7 +602,7 @@ g.test_null_as_last_value_gt_non_nullable = function() values_opts = { nil, {is_nullable = false}, - } + }, }, } @@ -638,4 +638,182 @@ return M]] t.assert_equals(filter_func({'a', box.NULL}), false) -- box.NULL > box.NULL is false end +g.test_jsonpath_fields_eq = function() + local filter_conditions = { + { + fields = {'[2].a.b'}, + operator = compare_conditions.operators.EQ, + values = {55}, + types = {'number'}, + early_exit_is_possible = true, + }, + } + + local expected_code = [[local tuple = ... + +local field__2__a_b = tuple["[2].a.b"] + +if not eq_1(field__2__a_b) then return false, true end + +return true, false]] + + local expected_library_code = [[local M = {} + +function M.eq_1(field__2__a_b) + return (eq(field__2__a_b, 55)) +end + +return M]] + + local filter_code = select_filters.internal.gen_filter_code(filter_conditions) + t.assert_equals(filter_code.code, expected_code) + t.assert_equals(filter_code.library, expected_library_code) + + local filter_func = select_filters.internal.compile(filter_code) + t.assert_equals({ filter_func(box.tuple.new({3, {a = {b = 55}}, 1})) }, {true, false}) + t.assert_equals({ filter_func(box.tuple.new({3, {a = {b = 23}}, 1})) }, {false, true}) + t.assert_equals({ filter_func(box.tuple.new({3, {a = {c = 55}}, 1})) }, {false, true}) + t.assert_equals({ filter_func(box.tuple.new({3, nil, 1})) }, {false, true}) +end + +g.test_jsonpath_fields_ge = function() + local filter_conditions = { + { + fields = {'[2]["field_2"]'}, + operator = compare_conditions.operators.GT, + values = {23}, + types = {'number'}, + early_exit_is_possible = true, + }, + } + + local expected_code = [[local tuple = ... + +local field__2___field_2__ = tuple["[2][\"field_2\"]"] + +if not cmp_1(field__2___field_2__) then return false, true end + +return true, false]] + + local expected_library_code = [[local M = {} + +function M.cmp_1(field__2___field_2__) + if lt(field__2___field_2__, 23) then return false end + if not eq(field__2___field_2__, 23) then return true end + + return false +end + +return M]] + + local filter_code = select_filters.internal.gen_filter_code(filter_conditions) + t.assert_equals(filter_code.code, expected_code) + t.assert_equals(filter_code.library, expected_library_code) + + local filter_func = select_filters.internal.compile(filter_code) + t.assert_equals({ filter_func(box.tuple.new({3, {field_2 = 55}, 1})) }, {true, false}) + t.assert_equals({ filter_func(box.tuple.new({3, {field_2 = 24, field_3 = 32}, nil})) }, {true, false}) + t.assert_equals({ filter_func(box.tuple.new({{field_2 = 59}, 173, 1})) }, {false, true}) +end + +g.test_several_jsonpath = function() + local filter_conditions = { + { + fields = {'[3]["f2"][\'f3\']', '[4].f3'}, + operator = compare_conditions.operators.EQ, + values = {'a', 'b'}, + types = {'string', 'string'}, + early_exit_is_possible = true, + }, + } + + local expected_code = [[local tuple = ... + +local field__3___f2____f3__ = tuple["[3][\"f2\"]['f3']"] +local field__4__f3 = tuple["[4].f3"] + +if not eq_1(field__3___f2____f3__, field__4__f3) then return false, true end + +return true, false]] + + local expected_library_code = [[local M = {} + +function M.eq_1(field__3___f2____f3__, field__4__f3) + return (eq(field__3___f2____f3__, "a") and eq(field__4__f3, "b")) +end + +return M]] + + local filter_code = select_filters.internal.gen_filter_code(filter_conditions) + t.assert_equals(filter_code.code, expected_code) + t.assert_equals(filter_code.library, expected_library_code) + + local filter_func = select_filters.internal.compile(filter_code) + t.assert_equals({ filter_func(box.tuple.new({1, 2, {f2 = {f3 = "a"}}, {f3 = "b"}})) }, {true, false}) + t.assert_equals({ filter_func(box.tuple.new({1, 2, {f3 = "b"}}, {f2 = {f3 = "a"}})) }, {false, true}) + t.assert_equals({ filter_func(box.tuple.new({1, 2, {f2 = {f3 = "a"}}, "b"})) }, {false, true}) +end + +g.test_jsonpath_two_conditions = function() + local filter_conditions = { + { + fields = {'[2].fld_1', '[3]["f_1"]'}, + operator = compare_conditions.operators.GE, + values = {"jsonpath_test", 23}, + types = {'string', 'number'}, + early_exit_is_possible = false, + }, + { + fields = {'[1].field_1'}, + operator = compare_conditions.operators.LT, + values = {8}, + types = {'number'}, + early_exit_is_possible = true, + }, + } + + local expected_code = [[local tuple = ... + +local field__2__fld_1 = tuple["[2].fld_1"] +local field__3___f_1__ = tuple["[3][\"f_1\"]"] +local field__1__field_1 = tuple["[1].field_1"] + +if not cmp_1(field__2__fld_1, field__3___f_1__) then return false, false end +if not cmp_2(field__1__field_1) then return false, true end + +return true, false]] + + local expected_library_code = [[local M = {} + +function M.cmp_1(field__2__fld_1, field__3___f_1__) + if lt(field__2__fld_1, "jsonpath_test") then return false end + if not eq(field__2__fld_1, "jsonpath_test") then return true end + + if lt(field__3___f_1__, 23) then return false end + if not eq(field__3___f_1__, 23) then return true end + + return true +end + +function M.cmp_2(field__1__field_1) + if lt(field__1__field_1, 8) then return true end + if not eq(field__1__field_1, 8) then return false end + + return false +end + +return M]] + + local filter_code = select_filters.internal.gen_filter_code(filter_conditions) + t.assert_equals(filter_code.code, expected_code) + t.assert_equals(filter_code.library, expected_library_code) + + local filter_func = select_filters.internal.compile(filter_code) + t.assert_equals({ filter_func(box.tuple.new({{field_1 = 7}, {fld_1 = "jsonpath_test"}, {f_1 = 23}})) }, {true, false}) + t.assert_equals({ filter_func(box.tuple.new({{field_1 = 8}, {fld_1 = "jsonpath_test"}, {f_1 = 23}})) }, {false, true}) + t.assert_equals({ filter_func(box.tuple.new({{field_1 = 5}, {f_1 = "jsonpath_test"}, {fld_1 = 23}})) }, {false, false}) + t.assert_equals({ filter_func(box.tuple.new({{field_1 = 5, f2 = 3}, {fld_1 = "jsonpath_test"}, 23})) }, {false, false}) +end + + -- luacheck: pop diff --git a/test/unit/select_filters_uuid_test.lua b/test/unit/select_filters_uuid_test.lua index 3228eb8a..256e7ba1 100644 --- a/test/unit/select_filters_uuid_test.lua +++ b/test/unit/select_filters_uuid_test.lua @@ -73,7 +73,7 @@ g.test_parse = function() -- uuid filter (early exit is possible) local uuid_filter_condition = filter_conditions[1] t.assert_type(uuid_filter_condition, 'table') - t.assert_equals(uuid_filter_condition.fieldnos, {1}) + t.assert_equals(uuid_filter_condition.fields, {1}) t.assert_equals(uuid_filter_condition.operator, compare_conditions.operators.LT) t.assert_equals(uuid_filter_condition.values, {uuid2}) t.assert_equals(uuid_filter_condition.types, {'uuid'}) @@ -82,7 +82,7 @@ g.test_parse = function() -- name filter local name_filter_condition = filter_conditions[2] t.assert_type(name_filter_condition, 'table') - t.assert_equals(name_filter_condition.fieldnos, {3}) + t.assert_equals(name_filter_condition.fields, {3}) t.assert_equals(name_filter_condition.operator, compare_conditions.operators.EQ) t.assert_equals(name_filter_condition.values, {'Charlie'}) t.assert_equals(name_filter_condition.types, {'string'}) @@ -91,7 +91,7 @@ g.test_parse = function() -- has_a_car filter local category_id_filter_condition = filter_conditions[3] t.assert_type(category_id_filter_condition, 'table') - t.assert_equals(category_id_filter_condition.fieldnos, {4}) + t.assert_equals(category_id_filter_condition.fields, {4}) t.assert_equals(category_id_filter_condition.operator, compare_conditions.operators.EQ) t.assert_equals(category_id_filter_condition.values, {uuid3}) t.assert_equals(category_id_filter_condition.types, {'uuid'}) @@ -111,7 +111,7 @@ g.test_one_condition_uuid = function() local filter_conditions = { { - fieldnos = {1}, + fields = {1}, operator = compare_conditions.operators.EQ, values = {uuid1}, types = {'uuid'}, @@ -153,7 +153,7 @@ g.test_one_condition_uuid_gt = function() local filter_conditions = { { - fieldnos = {1}, + fields = {1}, operator = compare_conditions.operators.GT, values = {uuid1}, types = {'uuid'}, @@ -198,7 +198,7 @@ g.test_one_condition_uuid_with_nil_value = function() local filter_conditions = { { - fieldnos = {1, 3}, + fields = {1, 3}, operator = compare_conditions.operators.GE, values = {uuid1}, types = {'uuid', 'string'}, @@ -206,7 +206,7 @@ g.test_one_condition_uuid_with_nil_value = function() values_opts = { {is_nullable = false}, {is_nullable = true}, - } + }, }, } @@ -246,14 +246,14 @@ g.test_two_conditions_uuid = function() local filter_conditions = { { - fieldnos = {2}, + fields = {2}, operator = compare_conditions.operators.EQ, values = {'Charlie'}, types = {'string'}, early_exit_is_possible = true, }, { - fieldnos = {3}, + fields = {3}, operator = compare_conditions.operators.GE, values = {uuid2:str()}, types = {'uuid'}, diff --git a/test/unit/select_plan_test.lua b/test/unit/select_plan_test.lua index dfe4d570..0d3f3beb 100644 --- a/test/unit/select_plan_test.lua +++ b/test/unit/select_plan_test.lua @@ -69,16 +69,6 @@ g.after_all(function() box.space.customers:drop() end) -g.test_bad_operand_name = function() - local plan, err = select_plan.new(box.space.customers, { - cond_funcs.gt('non-existent-field-index', 20), - }) - - t.assert_equals(plan, nil) - t.assert(err ~= nil) - t.assert_str_contains(err.err, 'No field or index "non-existent-field-index" found') -end - g.test_indexed_field = function() -- select by indexed field local conditions = { cond_funcs.gt('age', 20) }