Skip to content

Commit

Permalink
scan: support datetime filters
Browse files Browse the repository at this point in the history
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
  • Loading branch information
DifferentialOrange committed Mar 14, 2024
1 parent 453983b commit dd95975
Show file tree
Hide file tree
Showing 11 changed files with 811 additions and 61 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions crud/compare/filters.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
local datetime_supported, datetime = pcall(require, 'datetime')

local errors = require('errors')

local utils = require('crud.common.utils')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,

Expand Down
98 changes: 98 additions & 0 deletions test/entrypoint/srv_select/storage_init.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
local datetime_supported, _ = pcall(require, 'datetime')

local crud_utils = require('crud.common.utils')

return function()
Expand Down Expand Up @@ -227,4 +229,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
5 changes: 5 additions & 0 deletions test/helper.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 16 additions & 8 deletions test/integration/count_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
42 changes: 25 additions & 17 deletions test/integration/pairs_readview_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
38 changes: 23 additions & 15 deletions test/integration/pairs_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -893,23 +893,31 @@ 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
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')
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
Loading

0 comments on commit dd95975

Please sign in to comment.