From c1a52ebedede2f34cf34b7715dd2b7d30ece6369 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 11 Mar 2024 15:11:30 +0300 Subject: [PATCH] scan: support datetime filters Before this patch, datetime conditions were supported only in iterators, but not filters. (Any non-indexed condition or condition for any non-iterating index is a filter. Any condition index except for the first one is non-iterating.) This patch introduce datetime comparison support to filter codegen library, similar to uuid one. Part of #373 --- CHANGELOG.md | 6 + crud/compare/filters.lua | 50 ++++ test/entrypoint/srv_select/storage_init.lua | 97 +++++++ test/helper.lua | 5 + test/integration/count_test.lua | 24 +- test/integration/pairs_readview_test.lua | 42 +-- test/integration/pairs_test.lua | 39 ++- test/integration/read_scenario.lua | 270 ++++++++++++++++++ test/integration/select_readview_test.lua | 35 ++- test/integration/select_test.lua | 25 +- test/unit/select_filters_datetime_test.lua | 287 ++++++++++++++++++++ 11 files changed, 819 insertions(+), 61 deletions(-) create mode 100644 test/unit/select_filters_datetime_test.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 455a0727..338830d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed +* Working with datetime conditions in case of non-indexed fields or + non-iterating indexes (#373). + ## [1.4.3] - 05-02-24 ### Fixed diff --git a/crud/compare/filters.lua b/crud/compare/filters.lua index c2e39868..87ec0b9a 100644 --- a/crud/compare/filters.lua +++ b/crud/compare/filters.lua @@ -1,3 +1,5 @@ +local datetime_supported, datetime = pcall(require, 'datetime') + local errors = require('errors') local utils = require('crud.common.utils') @@ -159,6 +161,8 @@ local function format_value(value) return tostring(value) elseif utils.is_uuid(value) then return ("%q"):format(value) + elseif datetime_supported and datetime.is_datetime(value) then + return ("%q"):format(value:format()) elseif type(value) == 'cdata' then return tostring(value) end @@ -283,6 +287,8 @@ local function format_eq(cond) end elseif value_type == 'uuid' then func_name = 'eq_uuid' + elseif value_type == 'datetime' then + func_name = 'eq_datetime' end table.insert(cond_strings, format_comp_with_value(field, func_name, value)) @@ -309,6 +315,8 @@ local function format_lt(cond) func_name = add_collation_postfix('lt', value_opts) elseif value_type == 'uuid' then func_name = 'lt_uuid' + elseif value_type == 'datetime' then + func_name = 'lt_datetime' end func_name = add_strict_postfix(func_name, value_opts) @@ -533,6 +541,30 @@ local function lt_uuid_strict(lhs, rhs) return tostring(lhs) < tostring(rhs) end +local function opt_datetime_parse(v) + if type(v) == 'string' then + return datetime.parse(v) + end + + return v +end + +local function lt_datetime_nullable(lhs, rhs) + if lhs == nil and rhs ~= nil then + return true + elseif rhs == nil then + return false + end + return opt_datetime_parse(lhs) < opt_datetime_parse(rhs) +end + +local function lt_datetime_strict(lhs, rhs) + if rhs == nil then + return false + end + return opt_datetime_parse(lhs) < opt_datetime_parse(rhs) +end + local function lt_unicode_ci_nullable(lhs, rhs) if lhs == nil and rhs ~= nil then return true @@ -567,6 +599,20 @@ local function eq_uuid_strict(lhs, rhs) return tostring(lhs) == tostring(rhs) end +local function eq_datetime(lhs, rhs) + if lhs == nil then + return rhs == nil + end + return opt_datetime_parse(lhs) == opt_datetime_parse(rhs) +end + +local function eq_datetime_strict(lhs, rhs) + if rhs == nil then + return false + end + return opt_datetime_parse(lhs) == opt_datetime_parse(rhs) +end + local function eq_unicode_nullable(lhs, rhs) if lhs == nil and rhs == nil then return true @@ -604,6 +650,8 @@ local library = { eq = eq, eq_uuid = eq_uuid, eq_uuid_strict = eq_uuid_strict, + eq_datetime = eq_datetime, + eq_datetime_strict = eq_datetime_strict, -- nullable eq_unicode = eq_unicode_nullable, eq_unicode_ci = eq_unicode_ci_nullable, @@ -618,12 +666,14 @@ local library = { lt_unicode_ci = lt_unicode_ci_nullable, lt_boolean = lt_boolean_nullable, lt_uuid = lt_uuid_nullable, + lt_datetime = lt_datetime_nullable, -- strict lt_strict = lt_strict, lt_unicode_strict = lt_unicode_strict, lt_unicode_ci_strict = lt_unicode_ci_strict, lt_boolean_strict = lt_boolean_strict, lt_uuid_strict = lt_uuid_strict, + lt_datetime_strict = lt_datetime_strict, utf8 = utf8, diff --git a/test/entrypoint/srv_select/storage_init.lua b/test/entrypoint/srv_select/storage_init.lua index ce49d82e..6e66e9af 100644 --- a/test/entrypoint/srv_select/storage_init.lua +++ b/test/entrypoint/srv_select/storage_init.lua @@ -1,3 +1,4 @@ +local datetime_supported, _ = pcall(require, 'datetime') local crud_utils = require('crud.common.utils') return function() @@ -227,4 +228,100 @@ return function() unique = false, if_not_exists = true, }) + + if datetime_supported then + local datetime_format = { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'datetime_field', type = 'datetime'}, + } + + + local datetime_nonindexed_space = box.schema.space.create('datetime_nonindexed', { + if_not_exists = true, + engine = engine, + }) + + datetime_nonindexed_space:format(datetime_format) + + datetime_nonindexed_space:create_index('id_index', { + parts = { 'id' }, + if_not_exists = true, + }) + + datetime_nonindexed_space:create_index('bucket_id', { + parts = { 'bucket_id' }, + unique = false, + if_not_exists = true, + }) + + + local datetime_indexed_space = box.schema.space.create('datetime_indexed', { + if_not_exists = true, + engine = engine, + }) + + datetime_indexed_space:format(datetime_format) + + datetime_indexed_space:create_index('id_index', { + parts = { 'id' }, + if_not_exists = true, + }) + + datetime_indexed_space:create_index('bucket_id', { + parts = { 'bucket_id' }, + unique = false, + if_not_exists = true, + }) + + datetime_indexed_space:create_index('datetime_index', { + parts = { 'datetime_field' }, + unique = false, + if_not_exists = true, + }) + + + local datetime_pk_space = box.schema.space.create('datetime_pk', { + if_not_exists = true, + engine = engine, + }) + + datetime_pk_space:format(datetime_format) + + datetime_pk_space:create_index('datetime_index', { + parts = { 'datetime_field' }, + if_not_exists = true, + }) + + datetime_pk_space:create_index('bucket_id', { + parts = { 'bucket_id' }, + unique = false, + if_not_exists = true, + }) + + + local datetime_multipart_index_space = box.schema.space.create('datetime_multipart_index', { + if_not_exists = true, + engine = engine, + }) + + datetime_multipart_index_space:format(datetime_format) + + datetime_multipart_index_space:create_index('id_index', { + parts = { 'id' }, + if_not_exists = true, + }) + + datetime_multipart_index_space:create_index('bucket_id', { + parts = { 'bucket_id' }, + unique = false, + if_not_exists = true, + }) + + datetime_multipart_index_space:create_index('datetime_index', { + parts = { 'id', 'datetime_field' }, + unique = false, + if_not_exists = true, + }) + end end diff --git a/test/helper.lua b/test/helper.lua index 30c8cde3..67bd9b94 100644 --- a/test/helper.lua +++ b/test/helper.lua @@ -958,4 +958,9 @@ function helpers.prepare_ordered_data(g, space, expected_objects, bucket_id, ord t.assert_equals(objects, expected_objects) end +function helpers.skip_datetime_unsupported() + local module_available, _ = pcall(require, 'datetime') + t.skip_if(not module_available, 'datetime is not supported') +end + return helpers diff --git a/test/integration/count_test.lua b/test/integration/count_test.lua index 5047bd5f..575298ae 100644 --- a/test/integration/count_test.lua +++ b/test/integration/count_test.lua @@ -869,16 +869,24 @@ pgroup.test_count_force_map_call = function(g) t.assert_equals(result, 2) end +local read_impl = function(cg, space, conditions, opts) + opts = table.deepcopy(opts) or {} + opts.mode = 'write' + + local resp, err = cg.cluster.main_server:call('crud.count', {space, conditions, opts}) + t.assert_equals(err, nil) + + return resp +end + pgroup.test_gh_418_count_with_secondary_noneq_index_condition = function(g) - local read = function(cg, space, conditions, opts) - opts = table.deepcopy(opts) or {} - opts.mode = 'write' + read_scenario.gh_418_read_with_secondary_noneq_index_condition(g, read_impl) +end - local resp, err = cg.cluster.main_server:call('crud.count', {space, conditions, opts}) - t.assert_equals(err, nil) +for case_name_template, case in pairs(read_scenario.gh_373_read_with_datetime_condition_cases) do + local case_name = 'test_' .. case_name_template:format('count') - return resp + pgroup[case_name] = function(g) + case(g, read_impl) end - - read_scenario.gh_418_read_with_secondary_noneq_index_condition(g, read) end diff --git a/test/integration/pairs_readview_test.lua b/test/integration/pairs_readview_test.lua index 29ab2c4a..b35581c3 100644 --- a/test/integration/pairs_readview_test.lua +++ b/test/integration/pairs_readview_test.lua @@ -882,27 +882,35 @@ pgroup.test_pairs_no_map_reduce = function(g) t.assert_equals(diff_2, 0, 'Select request was not a map reduce') end -pgroup.test_gh_418_pairs_with_secondary_noneq_index_condition = function(g) - local read = function(cg, space, conditions, opts) - opts = table.deepcopy(opts) or {} - opts.use_tomap = true +local function read_impl(cg, space, conditions, opts) + opts = table.deepcopy(opts) or {} + opts.use_tomap = true - return cg.cluster.main_server:exec(function(space, conditions, opts) - local crud = require('crud') + return cg.cluster.main_server:exec(function(space, conditions, opts) + local crud = require('crud') - local rv, err = crud.readview() - t.assert_equals(err, nil) + local rv, err = crud.readview() + t.assert_equals(err, nil) - local status, resp = pcall(function() - return rv:pairs(space, conditions, opts):totable() - end) - t.assert(status, resp) + local status, resp = pcall(function() + return rv:pairs(space, conditions, opts):totable() + end) + t.assert(status, resp) - rv:close() + rv:close() - return resp, nil - end, {space, conditions, opts}) - end + return resp, nil + end, {space, conditions, opts}) +end - read_scenario.gh_418_read_with_secondary_noneq_index_condition(g, read) +pgroup.test_gh_418_pairs_with_secondary_noneq_index_condition = function(g) + read_scenario.gh_418_read_with_secondary_noneq_index_condition(g, read_impl) +end + +for case_name_template, case in pairs(read_scenario.gh_373_read_with_datetime_condition_cases) do + local case_name = 'test_' .. case_name_template:format('pairs') + + pgroup[case_name] = function(g) + case(g, read_impl) + end end diff --git a/test/integration/pairs_test.lua b/test/integration/pairs_test.lua index 60d1b527..6169ca67 100644 --- a/test/integration/pairs_test.lua +++ b/test/integration/pairs_test.lua @@ -893,23 +893,32 @@ pgroup.test_pairs_no_map_reduce = function(g) t.assert_equals(diff_2, 0, 'Select request was not a map reduce') end -pgroup.test_gh_418_pairs_with_secondary_noneq_index_condition = function(g) - local read = function(cg, space, conditions, opts) - opts = table.deepcopy(opts) or {} - opts.mode = 'write' - opts.use_tomap = true - return cg.cluster.main_server:exec(function(space, conditions, opts) - local crud = require('crud') +local function read_impl(cg, space, conditions, opts) + opts = table.deepcopy(opts) or {} + opts.mode = 'write' + opts.use_tomap = true + + return cg.cluster.main_server:exec(function(space, conditions, opts) + local crud = require('crud') - local status, resp = pcall(function() - return crud.pairs(space, conditions, opts):totable() - end) - t.assert(status, resp) + local status, resp = pcall(function() + return crud.pairs(space, conditions, opts):totable() + end) + t.assert(status, resp) - return resp, nil - end, {space, conditions, opts}) - end + return resp, nil + end, {space, conditions, opts}) +end - read_scenario.gh_418_read_with_secondary_noneq_index_condition(g, read) +pgroup.test_gh_418_pairs_with_secondary_noneq_index_condition = function(g) + read_scenario.gh_418_read_with_secondary_noneq_index_condition(g, read_impl) +end + +for case_name_template, case in pairs(read_scenario.gh_373_read_with_datetime_condition_cases) do + local case_name = 'test_' .. case_name_template:format('pairs') + + pgroup[case_name] = function(g) + case(g, read_impl) + end end diff --git a/test/integration/read_scenario.lua b/test/integration/read_scenario.lua index ff0e2462..f2c9ce60 100644 --- a/test/integration/read_scenario.lua +++ b/test/integration/read_scenario.lua @@ -5,6 +5,7 @@ -- Scenarios here are for `srv_select` entrypoint. local t = require('luatest') +local datetime_supported, datetime = pcall(require, 'datetime') local helpers = require('test.helper') @@ -61,6 +62,275 @@ local function gh_418_read_with_secondary_noneq_index_condition(cg, read) end end + +local function build_condition_case( + skip_test_condition, + space_name, + space_objects, + conditions, + expected_objects_without_bucket_id +) + return function(cg, read) + skip_test_condition() + + helpers.truncate_space_on_cluster(cg.cluster, space_name) + helpers.insert_objects(cg, space_name, space_objects) + + local result, err = read(cg, space_name, conditions) + t.assert_equals(err, nil) + + if type(result) == 'number' then -- crud.count + t.assert_equals(result, #expected_objects_without_bucket_id) + else + local actual_objects_without_bucket_id = {} + for k, v in pairs(result) do + v['bucket_id'] = nil + actual_objects_without_bucket_id[k] = v + end + + t.assert_items_equals(actual_objects_without_bucket_id, expected_objects_without_bucket_id) + end + end +end + + +local datetime_vals = {} + +if datetime_supported then + datetime_vals = { + yesterday = datetime.new{ + year = 2024, + month = 3, + day = 10, + }, + today = datetime.new{ + year = 2024, + month = 3, + day = 11, + }, + tomorrow = datetime.new{ + year = 2024, + month = 3, + day = 12, + }, + } + + assert(datetime_vals.yesterday < datetime_vals.today) + assert(datetime_vals.today < datetime_vals.tomorrow) +end + + +local datetime_data = { + { + id = 1, + datetime_field = datetime_vals.yesterday, + }, + { + id = 2, + datetime_field = datetime_vals.today, + }, + { + id = 3, + datetime_field = datetime_vals.tomorrow, + }, +} + + +local function today_condition(operator, operand, is_multipart) + if is_multipart then + return {operator, operand, {2, datetime_vals.today}} + else + return {operator, operand, datetime_vals.today} + end +end + +local datetime_condition_operator_options = { + single_lt = function(operand, is_multipart) + return { + conditions = {today_condition('<', operand, is_multipart)}, + expected_objects_without_bucket_id = { + { + id = 1, + datetime_field = datetime_vals.yesterday, + }, + }, + } + end, + single_le = function(operand, is_multipart) + return { + conditions = {today_condition('<=', operand, is_multipart)}, + expected_objects_without_bucket_id = { + { + id = 1, + datetime_field = datetime_vals.yesterday, + }, + { + id = 2, + datetime_field = datetime_vals.today, + }, + }, + } + end, + single_eq = function(operand, is_multipart) + return { + conditions = {today_condition('==', operand, is_multipart)}, + expected_objects_without_bucket_id = { + { + id = 2, + datetime_field = datetime_vals.today, + }, + }, + } + end, + single_ge = function(operand, is_multipart) + return { + conditions = {today_condition('>=', operand, is_multipart)}, + expected_objects_without_bucket_id = { + { + id = 2, + datetime_field = datetime_vals.today, + }, + { + id = 3, + datetime_field = datetime_vals.tomorrow, + }, + }, + } + end, + single_gt = function(operand, is_multipart) + return { + conditions = {today_condition('>', operand, is_multipart)}, + expected_objects_without_bucket_id = { + { + id = 3, + datetime_field = datetime_vals.tomorrow, + }, + }, + } + end, + second_lt = function(operand, is_multipart) + return { + conditions = {{'>=', 'id', 1}, today_condition('<', operand, is_multipart)}, + expected_objects_without_bucket_id = { + { + id = 1, + datetime_field = datetime_vals.yesterday, + }, + }, + } + end, + second_le = function(operand, is_multipart) + return { + conditions = {{'>=', 'id', 1}, today_condition('<=', operand, is_multipart)}, + expected_objects_without_bucket_id = { + { + id = 1, + datetime_field = datetime_vals.yesterday, + }, + { + id = 2, + datetime_field = datetime_vals.today, + }, + }, + } + end, + second_eq = function(operand, is_multipart) + return { + conditions = {{'>=', 'id', 1}, today_condition('==', operand, is_multipart)}, + expected_objects_without_bucket_id = { + { + id = 2, + datetime_field = datetime_vals.today, + }, + }, + } + end, + second_ge = function(operand, is_multipart) + return { + conditions = {{'>=', 'id', 1}, today_condition('>=', operand, is_multipart)}, + expected_objects_without_bucket_id = { + { + id = 2, + datetime_field = datetime_vals.today, + }, + { + id = 3, + datetime_field = datetime_vals.tomorrow, + }, + }, + } + end, + second_gt = function(operand, is_multipart) + return { + conditions = {{'>=', 'id', 1}, today_condition('>', operand, is_multipart)}, + expected_objects_without_bucket_id = { + { + id = 3, + datetime_field = datetime_vals.tomorrow, + }, + }, + } + end, +} + + +local datetime_condition_space_options = { + nonindexed = { + space_name = 'datetime_nonindexed', + index_kind = nil, + }, + indexed = { + space_name = 'datetime_indexed', + index_kind = 'secondary', + }, + pk = { + space_name = 'datetime_pk', + index_kind = 'primary', + }, + multipart_indexed = { + space_name = 'datetime_multipart_index', + index_kind = 'multipart', + is_multipart = true, + }, +} + + +local gh_373_read_with_datetime_condition_cases = {} + +for space_kind, space_option in pairs(datetime_condition_space_options) do + for operator_kind, operator_options_builder in pairs(datetime_condition_operator_options) do + local field_case_name_template = ('gh_373_%%s_with_datetime_%s_field_%s_condition'):format( + space_kind, operator_kind) + + local field_operator_options = operator_options_builder('datetime_field', false) + + gh_373_read_with_datetime_condition_cases[field_case_name_template] = build_condition_case( + helpers.skip_datetime_unsupported, + space_option.space_name, + datetime_data, + field_operator_options.conditions, + field_operator_options.expected_objects_without_bucket_id + ) + + if space_option.index_kind ~= nil then + local index_case_name_template = ('gh_373_%%s_with_datetime_%s_index_%s_condition'):format( + space_option.index_kind, operator_kind) + + local index_operator_options = operator_options_builder('datetime_index', space_option.is_multipart) + + gh_373_read_with_datetime_condition_cases[index_case_name_template] = build_condition_case( + helpers.skip_datetime_unsupported, + space_option.space_name, + datetime_data, + index_operator_options.conditions, + index_operator_options.expected_objects_without_bucket_id + ) + end + end +end + + return { gh_418_read_with_secondary_noneq_index_condition = gh_418_read_with_secondary_noneq_index_condition, + gh_373_read_with_datetime_condition_cases = gh_373_read_with_datetime_condition_cases, } diff --git a/test/integration/select_readview_test.lua b/test/integration/select_readview_test.lua index 796fcffe..4cfccdc7 100644 --- a/test/integration/select_readview_test.lua +++ b/test/integration/select_readview_test.lua @@ -2486,21 +2486,30 @@ pgroup.test_select_closed_readview = function(g) t.assert_str_contains(err.str, 'Read view is closed') end -pgroup.test_gh_418_select_with_secondary_noneq_index_condition = function(g) - local read = function(cg, space, conditions, opts) - return cg.cluster.main_server:exec(function(space, conditions, opts) - local crud = require('crud') - local rv, err = crud.readview() - t.assert_equals(err, nil) - local resp, err = rv:select(space, conditions, opts) - t.assert_equals(err, nil) +local function read_impl(cg, space, conditions, opts) + return cg.cluster.main_server:exec(function(space, conditions, opts) + local crud = require('crud') + local rv, err = crud.readview() + t.assert_equals(err, nil) - rv:close() + local resp, err = rv:select(space, conditions, opts) + t.assert_equals(err, nil) - return crud.unflatten_rows(resp.rows, resp.metadata) - end, {space, conditions, opts}) - end + rv:close() + + return crud.unflatten_rows(resp.rows, resp.metadata) + end, {space, conditions, opts}) +end - read_scenario.gh_418_read_with_secondary_noneq_index_condition(g, read) +pgroup.test_gh_418_select_with_secondary_noneq_index_condition = function(g) + read_scenario.gh_418_read_with_secondary_noneq_index_condition(g, read_impl) +end + +for case_name_template, case in pairs(read_scenario.gh_373_read_with_datetime_condition_cases) do + local case_name = 'test_' .. case_name_template:format('select') + + pgroup[case_name] = function(g) + case(g, read_impl) + end end diff --git a/test/integration/select_test.lua b/test/integration/select_test.lua index dff15ee8..4b23f178 100644 --- a/test/integration/select_test.lua +++ b/test/integration/select_test.lua @@ -2268,16 +2268,25 @@ pgroup.test_pairs_yield_every_0 = function(g) end) end + +local function read_impl(cg, space, conditions, opts) + opts = table.deepcopy(opts) or {} + opts.mode = 'write' + + local resp, err = cg.cluster.main_server:call('crud.select', {space, conditions, opts}) + t.assert_equals(err, nil) + + return crud.unflatten_rows(resp.rows, resp.metadata) +end + pgroup.test_gh_418_select_with_secondary_noneq_index_condition = function(g) - local read = function(cg, space, conditions, opts) - opts = table.deepcopy(opts) or {} - opts.mode = 'write' + read_scenario.gh_418_read_with_secondary_noneq_index_condition(g, read_impl) +end - local resp, err = cg.cluster.main_server:call('crud.select', {space, conditions, opts}) - t.assert_equals(err, nil) +for case_name_template, case in pairs(read_scenario.gh_373_read_with_datetime_condition_cases) do + local case_name = 'test_' .. case_name_template:format('select') - return crud.unflatten_rows(resp.rows, resp.metadata) + pgroup[case_name] = function(g) + case(g, read_impl) end - - read_scenario.gh_418_read_with_secondary_noneq_index_condition(g, read) end diff --git a/test/unit/select_filters_datetime_test.lua b/test/unit/select_filters_datetime_test.lua new file mode 100644 index 00000000..2159b6b0 --- /dev/null +++ b/test/unit/select_filters_datetime_test.lua @@ -0,0 +1,287 @@ +local _, datetime = pcall(require, 'datetime') + +local compare_conditions = require('crud.compare.conditions') +local cond_funcs = compare_conditions.funcs +local select_filters = require('crud.compare.filters') +local collations = require('crud.common.collations') +local select_plan = require('crud.compare.plan') + +local t = require('luatest') +local g = t.group('select_filters_datetime') + +local helpers = require('test.helper') + +g.before_all = function() + helpers.skip_datetime_unsupported() + + helpers.box_cfg() + + local customers_space = box.schema.space.create('customers', { + format = { + {'datetime', 'datetime'}, + {'bucket_id', 'unsigned'}, + {'name', 'string'}, + {'second_datetime', 'datetime'}, + }, + if_not_exists = true, + }) + customers_space:create_index('datetime', { -- id: 0 + parts = {'datetime'}, + if_not_exists = true, + }) + customers_space:create_index('second_datetime', { -- id: 1 + parts = { + { field = 'second_datetime', is_nullable = true }, + }, + if_not_exists = true, + }) +end + +g.after_all(function() + box.space.customers:drop() +end) + +g.test_parse = function() + -- select by indexed field with conditions by index and field + local dt1 = datetime.new{year = 2000, month = 1, day = 1, tz = 'Europe/Moscow'} + local dt2 = datetime.new{year = 2012, month = 1, day = 1, tzoffset = -180} + local dt3 = datetime.new{year = 1980, month = 1, day = 1} + + local conditions = { + cond_funcs.gt('datetime', dt1), + cond_funcs.lt('datetime', dt2), + cond_funcs.eq('name', 'Charlie'), + cond_funcs.eq('second_datetime', dt3) + } + + local plan, err = select_plan.new(box.space.customers, conditions) + t.assert_equals(err, nil) + + local space = box.space.customers + local scan_index = space.index[plan.index_id] + + local filter_conditions, err = select_filters.internal.parse(space, scan_index, conditions, { + scan_condition_num = plan.scan_condition_num, + tarantool_iter = plan.tarantool_iter, + }) + t.assert_equals(err, nil) + + -- datetime filter (early exit is possible) + local datetime_filter_condition = filter_conditions[1] + t.assert_type(datetime_filter_condition, 'table') + t.assert_equals(datetime_filter_condition.fields, {1}) + t.assert_equals(datetime_filter_condition.operator, compare_conditions.operators.LT) + t.assert_equals(datetime_filter_condition.values, {dt2}) + t.assert_equals(datetime_filter_condition.types, {'datetime'}) + t.assert_equals(datetime_filter_condition.early_exit_is_possible, true) + + -- name filter + local name_filter_condition = filter_conditions[2] + t.assert_type(name_filter_condition, 'table') + 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'}) + t.assert_equals(name_filter_condition.early_exit_is_possible, false) + + -- second_datetime filter + local second_datetime_filter_condition = filter_conditions[3] + t.assert_type(second_datetime_filter_condition, 'table') + t.assert_equals(second_datetime_filter_condition.fields, {4}) + t.assert_equals(second_datetime_filter_condition.operator, compare_conditions.operators.EQ) + t.assert_equals(second_datetime_filter_condition.values, {dt3}) + t.assert_equals(second_datetime_filter_condition.types, {'datetime'}) + t.assert_equals(second_datetime_filter_condition.early_exit_is_possible, false) + + t.assert_equals(#second_datetime_filter_condition.values_opts, 1) + local second_datetime_opts = second_datetime_filter_condition.values_opts[1] + t.assert_equals(second_datetime_opts.is_nullable, true) + t.assert_equals(second_datetime_opts.collation, collations.NONE) +end + +g.test_one_condition_datetime = function() + local dt1 = datetime.new{year = 2000, month = 1, day = 1, tz = 'Europe/Moscow'} + local dt2 = datetime.new{year = 2012, month = 1, day = 1, tzoffset = -180} + + local filter_conditions = { + { + fields = {1}, + operator = compare_conditions.operators.EQ, + values = {dt1}, + types = {'datetime'}, + early_exit_is_possible = true, + } + } + + local expected_code = [[local tuple = ... + +local field_1 = tuple[1] + +if not eq_1(field_1) then return false, true end + +return true, false]] + + local expected_library_code = [[local M = {} + +function M.eq_1(field_1) + return (eq_datetime(field_1, "2000-01-01T00:00:00 Europe/Moscow")) +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({dt1, dt1:format(), 1}) }, {true, false}) + t.assert_equals({ filter_func({dt2, dt1:format(), 1}) }, {false, true}) + t.assert_equals({ filter_func({nil, dt1:format(), 1}) }, {false, true}) +end + +g.test_one_condition_datetime_gt = function() + local dt1 = datetime.new{year = 2000, month = 1, day = 1, tz = 'Europe/Moscow'} + local dt2 = datetime.new{year = 2012, month = 1, day = 1, tzoffset = -180} + + local filter_conditions = { + { + fields = {1}, + operator = compare_conditions.operators.GT, + values = {dt1}, + types = {'datetime'}, + early_exit_is_possible = true, + } + } + + local expected_code = [[local tuple = ... + +local field_1 = tuple[1] + +if not cmp_1(field_1) then return false, true end + +return true, false]] + + local expected_library_code = [[local M = {} + +function M.cmp_1(field_1) + if lt_datetime(field_1, "2000-01-01T00:00:00 Europe/Moscow") then return false end + if not eq_datetime(field_1, "2000-01-01T00:00:00 Europe/Moscow") 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({dt2, dt1:format(), 1}) }, {true, false}) + t.assert_equals({ filter_func({dt1, dt2:format(), 1}) }, {false, true}) + t.assert_equals({ filter_func({nil, dt1:format(), 1}) }, {false, true}) +end + +g.test_one_condition_datetime_with_nil_value = function() + local dt1 = datetime.new{year = 2000, month = 1, day = 1, tz = 'Europe/Moscow'} + local dt2 = datetime.new{year = 2012, month = 1, day = 1, tzoffset = -180} + + local filter_conditions = { + { + fields = {1, 3}, + operator = compare_conditions.operators.GE, + values = {dt1}, + types = {'datetime', 'string'}, + early_exit_is_possible = false, + values_opts = { + {is_nullable = false}, + {is_nullable = true}, + }, + }, + } + + local expected_code = [[local tuple = ... + +local field_1 = tuple[1] + +if not cmp_1(field_1) then return false, false end + +return true, false]] + + local expected_library_code = [[local M = {} + +function M.cmp_1(field_1) + if lt_datetime_strict(field_1, "2000-01-01T00:00:00 Europe/Moscow") then return false end + if not eq_datetime(field_1, "2000-01-01T00:00:00 Europe/Moscow") then return true end + + return true +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({dt1, 'test', 3}), true) + t.assert_equals(filter_func({dt2, 'xxx', 1}), true) +end + +g.test_two_conditions_datetime = function() + local dt1 = datetime.new{year = 2000, month = 1, day = 1, tz = 'Europe/Moscow'} + local dt2 = datetime.new{year = 2012, month = 1, day = 1, tzoffset = -180} + + local filter_conditions = { + { + fields = {2}, + operator = compare_conditions.operators.EQ, + values = {'Charlie'}, + types = {'string'}, + early_exit_is_possible = true, + }, + { + fields = {3}, + operator = compare_conditions.operators.GE, + values = {dt2:format()}, + types = {'datetime'}, + early_exit_is_possible = false, + } + } + + local expected_code = [[local tuple = ... + +local field_2 = tuple[2] +local field_3 = tuple[3] + +if not eq_1(field_2) then return false, true end +if not cmp_2(field_3) then return false, false end + +return true, false]] + + local expected_library_code = [[local M = {} + +function M.eq_1(field_2) + return (eq(field_2, "Charlie")) +end + +function M.cmp_2(field_3) + if lt_datetime(field_3, "2012-01-01T00:00:00-0300") then return false end + if not eq_datetime(field_3, "2012-01-01T00:00:00-0300") then return true end + + return true +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({4, 'xxx', dt1}) }, {false, true}) + t.assert_equals({ filter_func({5, 'Charlie', dt1}) }, {false, false}) + t.assert_equals({ filter_func({5, 'xxx', dt2}) }, {false, true}) + t.assert_equals({ filter_func({6, 'Charlie', dt2}) }, {true, false}) + t.assert_equals({ filter_func({6, 'Charlie', nil}) }, {false, false}) +end