Skip to content

Commit 3d2a71e

Browse files
authored
Bug: Collations (#21)
1 parent 60f5421 commit 3d2a71e

File tree

8 files changed

+198
-48
lines changed

8 files changed

+198
-48
lines changed

crud/common/collations.lua

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
local json = require('json')
2+
3+
local COLLATION_NAME_FN = 2
4+
5+
local collations = {}
6+
7+
collations.NONE = 'none'
8+
collations.BINARY = 'binary'
9+
collations.UNICODE = 'unicode'
10+
collations.UNICODE_CI = 'unicode_ci'
11+
12+
function collations.get(index_part)
13+
if index_part.collation ~= nil then
14+
return index_part.collation
15+
end
16+
17+
if index_part.collation_id == nil then
18+
return collations.NONE
19+
end
20+
21+
local collation_tuple = box.space._collation:get(index_part.collation_id)
22+
assert(collation_tuple ~= nil, "Unknown collation_id: " .. json.encode(index_part.collation_id))
23+
24+
local collation = collation_tuple[COLLATION_NAME_FN]
25+
return collation
26+
end
27+
28+
function collations.is_default(collation)
29+
if collation == nil then
30+
return true
31+
end
32+
33+
if collation == collations.NONE or collation == collations.BINARY then
34+
return true
35+
end
36+
37+
return false
38+
end
39+
40+
function collations.is_unicode(collation)
41+
if collation == nil then
42+
return false
43+
end
44+
45+
return string.startswith(collation, 'unicode_')
46+
end
47+
48+
return collations

crud/select/comparators.lua

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
local errors = require('errors')
22

3+
local collations = require('crud.common.collations')
34
local select_conditions = require('crud.select.conditions')
45
local operators = select_conditions.operators
56

@@ -131,17 +132,18 @@ local function gen_array_cmp_func(target, key_parts)
131132
local eq_funcs = {}
132133

133134
for _, part in ipairs(key_parts) do
134-
if part.collation == nil then
135+
local collation = collations.get(part)
136+
if collations.is_default(collation) then
135137
table.insert(lt_funcs, lt)
136138
table.insert(eq_funcs, eq)
137-
elseif part.collation == 'unicode' then
139+
elseif collation == collations.UNICODE then
138140
table.insert(lt_funcs, lt_unicode)
139141
table.insert(eq_funcs, eq_unicode)
140-
elseif part.collation == 'unicode_ci' then
142+
elseif collation == collations.UNICODE_CI then
141143
table.insert(lt_funcs, lt_unicode_ci)
142144
table.insert(eq_funcs, eq_unicode_ci)
143145
else
144-
return nil, GenFuncError:new('Unsupported tarantool collation %q', part.collation)
146+
return nil, GenFuncError:new('Unsupported Tarantool collation %q', collation)
145147
end
146148
end
147149

@@ -175,7 +177,7 @@ local array_cmp_funcs_by_operators = {
175177
--]=]
176178
function comparators.get_cmp_operator(tarantool_iter)
177179
local cmp_operator = cmp_operators_by_tarantool_iter[tarantool_iter]
178-
assert(cmp_operator ~= nil, 'Unknown Tarantool iterator %q', tarantool_iter)
180+
assert(cmp_operator ~= nil, 'Unsupported Tarantool iterator %q', tarantool_iter)
179181

180182
return cmp_operator
181183
end

crud/select/filters.lua

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local collations = require('crud.common.collations')
12
local select_conditions = require('crud.select.conditions')
23

34
local function format_value(value)
@@ -73,6 +74,30 @@ local function format_comp_with_value(fieldno, func_name, value)
7374
)
7475
end
7576

77+
local function add_strict_postfix(func_name, value_opts)
78+
if value_opts.is_nullable == false then
79+
return string.format('%s_strict', func_name)
80+
end
81+
82+
return func_name
83+
end
84+
85+
local function add_collation_postfix(func_name, value_opts)
86+
if collations.is_default(value_opts.collation) then
87+
return func_name
88+
end
89+
90+
if value_opts.collation == collations.UNICODE then
91+
return string.format('%s_unicode', func_name)
92+
end
93+
94+
if value_opts.collation == collations.UNICODE_CI then
95+
return string.format('%s_unicode_ci', func_name)
96+
end
97+
98+
error('Unsupported collation: ' .. tostring(value_opts.collation))
99+
end
100+
76101
local function format_eq(cond)
77102
local cond_strings = {}
78103
local values_opts = cond.values_opts or {}
@@ -82,22 +107,11 @@ local function format_eq(cond)
82107
local value = cond.values[j]
83108
local value_opts = values_opts[j] or {}
84109

85-
local postfix = ''
86-
if value_opts.is_nullable == false then
87-
postfix = '_strict'
88-
end
110+
local func_name = 'eq'
111+
func_name = add_collation_postfix(func_name, value_opts)
89112

90-
local func_name
91-
if value_opts.collation ~= nil then
92-
if value_opts.collation == 'unicode' then
93-
func_name = 'eq_unicode' .. postfix
94-
elseif value_opts.collation == 'unicode_ci' then
95-
func_name = 'eq_unicode_ci' .. postfix
96-
else
97-
error('unknown collation: ' .. tostring(value_opts.collation))
98-
end
99-
else
100-
func_name = 'eq'
113+
if collations.is_unicode(value_opts.collation) then
114+
func_name = add_strict_postfix(func_name, value_opts)
101115
end
102116

103117
table.insert(cond_strings, format_comp_with_value(fieldno, func_name, value))
@@ -116,25 +130,9 @@ local function format_lt(cond)
116130
local value_type = cond.types[j]
117131
local value_opts = values_opts[j] or {}
118132

119-
local postfix = ''
120-
if value_opts.is_nullable == false then
121-
postfix = '_strict'
122-
end
123-
124-
local func_name
125-
if value_opts.collation ~= nil then
126-
if value_opts.collation == 'unicode' then
127-
func_name = 'lt_unicode' .. postfix
128-
elseif value_opts.collation == 'unicode_ci' then
129-
func_name = 'lt_unicode_ci' .. postfix
130-
else
131-
error('unknown collation: ' .. tostring(value_opts.collation))
132-
end
133-
elseif value_type == 'boolean' then
134-
func_name = 'lt_boolean' .. postfix
135-
else
136-
func_name = 'lt' .. postfix
137-
end
133+
local func_name = value_type ~= 'boolean' and 'lt' or 'lt_boolean'
134+
func_name = add_collation_postfix(func_name, value_opts)
135+
func_name = add_strict_postfix(func_name, value_opts)
138136

139137
table.insert(cond_strings, format_comp_with_value(fieldno, func_name, value))
140138
end
@@ -405,7 +403,7 @@ local library = {
405403
-- LT
406404
-- nullable
407405
lt = lt_nullable,
408-
lt_unicode_ = lt_unicode_nullable,
406+
lt_unicode = lt_unicode_nullable,
409407
lt_unicode_ci = lt_unicode_ci_nullable,
410408
lt_boolean = lt_boolean_nullable,
411409
-- strict

crud/select/plan.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
local errors = require('errors')
22
local json = require('json')
33

4+
local collations = require('crud.common.collations')
45
local select_conditions = require('crud.select.conditions')
56

67
local select_plan = {}
@@ -180,7 +181,7 @@ local function get_values_opts(index, fieldnos)
180181
assert(index_part ~= nil)
181182

182183
is_nullable = index_part.is_nullable
183-
collation = index_part.collation
184+
collation = collations.get(index_part)
184185
end
185186

186187
table.insert(values_opts, {

test/entrypoint/srv_select.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ package.preload['customers-storage'] = function()
4141
customers_space:create_index('full_name', {
4242
parts = {
4343
{ field = 'name', collation = 'unicode_ci' },
44-
{ field = 'last_name', is_nullable = true },
44+
{ field = 'last_name', collation = 'unicode_ci' },
4545
},
4646
unique = false,
4747
if_not_exists = true,

test/integration/select_test.lua

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,3 +751,57 @@ add('test_select_by_full_sharding_key', function(g)
751751
t.assert_equals(err, nil)
752752
t.assert_equals(objects, get_by_ids(customers, {3}))
753753
end)
754+
755+
add('test_select_with_collations', function(g)
756+
local customers = insert_customers(g, {
757+
{
758+
id = 1, name = "Elizabeth", last_name = "Jackson",
759+
age = 12, city = "Oxford",
760+
}, {
761+
id = 2, name = "Mary", last_name = "Brown",
762+
age = 46, city = "oxford",
763+
}, {
764+
id = 3, name = "elizabeth", last_name = "brown",
765+
age = 46, city = "Oxford",
766+
}, {
767+
id = 4, name = "Jack", last_name = "Sparrow",
768+
age = 35, city = "oxford",
769+
}, {
770+
id = 5, name = "William", last_name = "Terner",
771+
age = 25, city = "Oxford",
772+
}, {
773+
id = 6, name = "elizabeth", last_name = "Brown",
774+
age = 33, city = "Los Angeles",
775+
},
776+
})
777+
778+
table.sort(customers, function(obj1, obj2) return obj1.id < obj2.id end)
779+
780+
-- full name index - unicode ci collation (case-insensitive)
781+
local conditions = {{'==', 'name', "Elizabeth"}}
782+
local objects, err = g.cluster.main_server.net_box:eval([[
783+
local crud = require('crud')
784+
785+
local conditions = ...
786+
787+
local objects, err = crud.select('customers', conditions)
788+
return objects, err
789+
]], {conditions})
790+
791+
t.assert_equals(err, nil)
792+
t.assert_equals(objects, get_by_ids(customers, {3, 6, 1}))
793+
794+
-- city - no collation (case-sensitive)
795+
local conditions = {{'==', 'city', "oxford"}}
796+
local objects, err = g.cluster.main_server.net_box:eval([[
797+
local crud = require('crud')
798+
799+
local conditions = ...
800+
801+
local objects, err = crud.select('customers', conditions)
802+
return objects, err
803+
]], {conditions})
804+
805+
t.assert_equals(err, nil)
806+
t.assert_equals(objects, get_by_ids(customers, {2, 4}))
807+
end)

test/unit/select_filters_test.lua

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ g.test_unicode_collation = function()
311311
types = {'string', 'string', 'string', 'number'},
312312
early_exit_is_possible = false,
313313
values_opts = {
314-
{collation='unicode_ci'},
314+
{collation='unicode'},
315315
{collation='unicode_ci'},
316316
{collation='unicode_ci'},
317317
}
@@ -332,7 +332,7 @@ return true, false]]
332332
local expected_library_code = [[local M = {}
333333
334334
function M.eq_1(field_1, field_2, field_3, field_4)
335-
return (eq_unicode_ci(field_1, "A") and eq_unicode_ci(field_2, "Á") and eq_unicode_ci(field_3, "Ä") and eq(field_4, 6))
335+
return (eq_unicode(field_1, "A") and eq_unicode_ci(field_2, "Á") and eq_unicode_ci(field_3, "Ä") and eq(field_4, 6))
336336
end
337337
338338
return M]]
@@ -343,8 +343,54 @@ return M]]
343343

344344
local func = select_filters.compile(filter)
345345
t.assert_equals(func({'A', 'Á', 'Ä', 6}), true)
346-
t.assert_equals(func({'a', 'á', 'ä', 6}), true)
347-
t.assert_equals(func({'a', 'V', 'ä', 6}), false)
346+
t.assert_equals(func({'A', 'á', 'ä', 6}), true)
347+
t.assert_equals(func({'a', 'Á', 'Ä', 6}), false)
348+
t.assert_equals(func({'A', 'V', 'ä', 6}), false)
349+
end
350+
351+
g.test_binary_and_none_collation = function()
352+
local filter_conditions = {
353+
{
354+
fieldnos = {1, 2, 3},
355+
operator = select_conditions.operators.EQ,
356+
values = {'A', 'B', 'C'},
357+
types = {'string', 'string', 'string'},
358+
early_exit_is_possible = false,
359+
values_opts = {
360+
{collation='none'},
361+
{collation='binary'},
362+
{collation=nil},
363+
}
364+
},
365+
}
366+
367+
local expected_code = [[local tuple = ...
368+
369+
local field_1 = tuple[1]
370+
local field_2 = tuple[2]
371+
local field_3 = tuple[3]
372+
373+
if not eq_1(field_1, field_2, field_3) then return false, false end
374+
375+
return true, false]]
376+
377+
local expected_library_code = [[local M = {}
378+
379+
function M.eq_1(field_1, field_2, field_3)
380+
return (eq(field_1, "A") and eq(field_2, "B") and eq(field_3, "C"))
381+
end
382+
383+
return M]]
384+
385+
local filter = select_filters.gen_code(filter_conditions)
386+
t.assert_equals(filter.code, expected_code)
387+
t.assert_equals(filter.library_code, expected_library_code)
388+
389+
local func = select_filters.compile(filter)
390+
t.assert_equals(func({'A', 'B', 'C'}), true)
391+
t.assert_equals(func({'a', 'B', 'C'}), false)
392+
t.assert_equals(func({'A', 'b', 'C'}), false)
393+
t.assert_equals(func({'A', 'B', 'c'}), false)
348394
end
349395

350396
g.test_null_as_last_value_eq = function()

test/unit/select_plan_test.lua

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
local select_plan = require('crud.select.plan')
22

3+
local collations = require('crud.common.collations')
34
local select_conditions = require('crud.select.conditions')
45
local cond_funcs = select_conditions.funcs
56

@@ -228,12 +229,12 @@ g.test_filter_conditions = function()
228229
-- - name part opts
229230
local name_opts = full_name_values_opts[1]
230231
t.assert_equals(name_opts.is_nullable, false)
231-
t.assert_equals(name_opts.collation, 'unicode_ci')
232+
t.assert_equals(name_opts.collation, collations.UNICODE_CI)
232233

233234
-- - last_name part opts
234235
local last_name_opts = full_name_values_opts[2]
235236
t.assert_equals(last_name_opts.is_nullable, true)
236-
t.assert_equals(last_name_opts.collation, nil)
237+
t.assert_equals(last_name_opts.collation, collations.NONE)
237238

238239
-- has_a_car filter
239240
local has_a_car_filter_condition = plan.filter_conditions[3]

0 commit comments

Comments
 (0)