Skip to content

Commit 6021b72

Browse files
authored
Select: jsonpath support (#150)
Added jsonpath support for ``crud.select``. Part of #12
1 parent 2bf9fb8 commit 6021b72

File tree

12 files changed

+384
-119
lines changed

12 files changed

+384
-119
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
3232
* Option flag `force_map_call` for `select()`/`pairs()`
3333
to disable the `bucket_id` computation from primary key.
3434
* `crud.min` and `crud.max` functions to find the minimum and maximum values in the specified index.
35+
* Added support for jsonpath for select.
3536

3637
## [0.6.0] - 2021-03-29
3738

crud/compare/conditions.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ for func_name, operator in pairs(cond_operators_by_func_names) do
6363
return new_condition({
6464
operator = operator,
6565
operand = operand,
66-
values = values
66+
values = values,
6767
})
6868
end
6969
end

crud/select/filters.lua

Lines changed: 65 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
local json = require('json')
21
local errors = require('errors')
32

43
local utils = require('crud.common.utils')
54
local dev_checks = require('crud.common.dev_checks')
65
local collations = require('crud.common.collations')
76
local compare_conditions = require('crud.compare.conditions')
87

9-
local ParseConditionsError = errors.new_class('ParseConditionsError', {capture_stack = false})
108
local GenFiltersError = errors.new_class('GenFiltersError', {capture_stack = false})
119

1210
local filters = {}
@@ -97,31 +95,42 @@ local function parse(space, conditions, opts)
9795
for i, condition in ipairs(conditions) do
9896
if i ~= opts.scan_condition_num then
9997
-- Index check (including one and multicolumn)
100-
local fieldnos
101-
local fields_types
98+
local fields
99+
local fields_types = {}
102100
local values_opts
103101

104102
local index = space_indexes[condition.operand]
105103

106104
if index ~= nil then
107-
fieldnos = get_index_fieldnos(index)
105+
fields = get_index_fieldnos(index)
108106
fields_types = get_index_fields_types(index)
109107
values_opts = get_values_opts(index)
110-
elseif fieldnos_by_names[condition.operand] ~= nil then
111-
local fiendno = fieldnos_by_names[condition.operand]
112-
fieldnos = {fiendno}
113-
local field_format = space_format[fiendno]
114-
fields_types = {field_format.type}
115-
local is_nullable = field_format.is_nullable == true
108+
else
109+
local fieldno = fieldnos_by_names[condition.operand]
110+
111+
if fieldno ~= nil then
112+
fields = {fieldno}
113+
else
114+
-- We assume this is jsonpath, so it is
115+
-- not in fieldnos_by_name map.
116+
fields = {condition.operand}
117+
end
118+
119+
local field_format = space_format[fieldno]
120+
local is_nullable
121+
122+
if field_format ~= nil then
123+
fields_types = {field_format.type}
124+
is_nullable = field_format.is_nullable == true
125+
end
126+
116127
values_opts = {
117128
{is_nullable = is_nullable, collation = nil},
118129
}
119-
else
120-
return nil, ParseConditionsError('No field or index is found for condition %s', json.encode(condition))
121130
end
122131

123132
table.insert(filter_conditions, {
124-
fieldnos = fieldnos,
133+
fields = fields,
125134
operator = condition.operator,
126135
values = condition.values,
127136
types = fields_types,
@@ -156,12 +165,30 @@ end
156165
local PARSE_ARGS_TEMPLATE = 'local tuple = ...'
157166
local LIB_FUNC_HEADER_TEMPLATE = 'function M.%s(%s)'
158167

168+
local function format_path(path)
169+
local path_type = type(path)
170+
if path_type == 'number' then
171+
return tostring(path)
172+
elseif path_type == 'string' then
173+
return ('%q'):format(path)
174+
end
175+
176+
assert(false, ('Unexpected format: %s'):format(path_type))
177+
end
178+
159179
local function concat_conditions(conditions, operator)
160180
return '(' .. table.concat(conditions, (' %s '):format(operator)) .. ')'
161181
end
162182

163-
local function get_field_variable_name(fieldno)
164-
return string.format('field_%s', fieldno)
183+
local function get_field_variable_name(field)
184+
local field_type = type(field)
185+
if field_type == 'number' then
186+
field = tostring(field)
187+
elseif field_type == 'string' then
188+
field = string.gsub(field, '([().^$%[%]%+%-%*%?%%\'"])', '_')
189+
end
190+
191+
return string.format('field_%s', field)
165192
end
166193

167194
local function get_eq_func_name(id)
@@ -173,38 +200,39 @@ local function get_cmp_func_name(id)
173200
end
174201

175202
local function gen_tuple_fields_def_code(filter_conditions)
176-
-- get field numbers
177-
local fieldnos_added = {}
178-
local fieldnos = {}
203+
-- get field names
204+
local fields_added = {}
205+
local fields = {}
179206

180207
for _, cond in ipairs(filter_conditions) do
181208
for i = 1, #cond.values do
182-
local fieldno = cond.fieldnos[i]
183-
if not fieldnos_added[fieldno] then
184-
table.insert(fieldnos, fieldno)
185-
fieldnos_added[fieldno] = true
209+
local field = cond.fields[i]
210+
211+
if not fields_added[field] then
212+
table.insert(fields, field)
213+
fields_added[field] = true
186214
end
187215
end
188216
end
189217

190218
-- gen definitions for all used fields
191219
local fields_def_parts = {}
192220

193-
for _, fieldno in ipairs(fieldnos) do
221+
for _, field in ipairs(fields) do
194222
table.insert(fields_def_parts, string.format(
195223
'local %s = tuple[%s]',
196-
get_field_variable_name(fieldno), fieldno
224+
get_field_variable_name(field), format_path(field)
197225
))
198226
end
199227

200228
return table.concat(fields_def_parts, '\n')
201229
end
202230

203-
local function format_comp_with_value(fieldno, func_name, value)
231+
local function format_comp_with_value(field, func_name, value)
204232
return string.format(
205233
'%s(%s, %s)',
206234
func_name,
207-
get_field_variable_name(fieldno),
235+
get_field_variable_name(field),
208236
format_value(value)
209237
)
210238
end
@@ -238,7 +266,7 @@ local function format_eq(cond)
238266
local values_opts = cond.values_opts or {}
239267

240268
for j = 1, #cond.values do
241-
local fieldno = cond.fieldnos[j]
269+
local field = cond.fields[j]
242270
local value = cond.values[j]
243271
local value_type = cond.types[j]
244272
local value_opts = values_opts[j] or {}
@@ -254,7 +282,7 @@ local function format_eq(cond)
254282
func_name = 'eq_uuid'
255283
end
256284

257-
table.insert(cond_strings, format_comp_with_value(fieldno, func_name, value))
285+
table.insert(cond_strings, format_comp_with_value(field, func_name, value))
258286
end
259287

260288
return cond_strings
@@ -265,7 +293,7 @@ local function format_lt(cond)
265293
local values_opts = cond.values_opts or {}
266294

267295
for j = 1, #cond.values do
268-
local fieldno = cond.fieldnos[j]
296+
local field = cond.fields[j]
269297
local value = cond.values[j]
270298
local value_type = cond.types[j]
271299
local value_opts = values_opts[j] or {}
@@ -279,9 +307,10 @@ local function format_lt(cond)
279307
elseif value_type == 'uuid' then
280308
func_name = 'lt_uuid'
281309
end
310+
282311
func_name = add_strict_postfix(func_name, value_opts)
283312

284-
table.insert(cond_strings, format_comp_with_value(fieldno, func_name, value))
313+
table.insert(cond_strings, format_comp_with_value(field, func_name, value))
285314
end
286315

287316
return cond_strings
@@ -366,10 +395,10 @@ local function gen_cmp_array_func_code(operator, func_name, cond, func_args_code
366395
return table.concat(func_code_lines, '\n')
367396
end
368397

369-
local function function_args_by_fieldnos(fieldnos)
398+
local function function_args_by_field(fields)
370399
local arg_names = {}
371-
for _, fieldno in ipairs(fieldnos) do
372-
table.insert(arg_names, get_field_variable_name(fieldno))
400+
for _, field in ipairs(fields) do
401+
table.insert(arg_names, get_field_variable_name(field))
373402
end
374403
return table.concat(arg_names, ', ')
375404
end
@@ -408,8 +437,8 @@ local function gen_filter_code(filter_conditions)
408437
table.insert(filter_code_parts, '')
409438

410439
for i, cond in ipairs(filter_conditions) do
411-
local args_fieldnos = { unpack(cond.fieldnos, 1, #cond.values) }
412-
local func_args_code = function_args_by_fieldnos(args_fieldnos)
440+
local args_fields = { unpack(cond.fields, 1, #cond.values) }
441+
local func_args_code = function_args_by_field(args_fields)
413442

414443
local library_func_name, library_func_code = gen_library_func(i, cond, func_args_code)
415444
table.insert(library_funcs_code_parts, library_func_code)

crud/select/plan.lua

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ local dev_checks = require('crud.common.dev_checks')
66

77
local select_plan = {}
88

9-
local SelectPlanError = errors.new_class('SelectPlanError', {capture_stack = false})
109
local IndexTypeError = errors.new_class('IndexTypeError', {capture_stack = false})
11-
local ValidateConditionsError = errors.new_class('ValidateConditionsError', {capture_stack = false})
1210
local FilterFieldsError = errors.new_class('FilterFieldsError', {capture_stack = false})
1311

1412
local function index_is_allowed(index)
@@ -42,34 +40,6 @@ local function get_index_for_condition(space_indexes, space_format, condition)
4240
end
4341
end
4442

45-
local function validate_conditions(conditions, space_indexes, space_format)
46-
local field_names = {}
47-
for _, field_format in ipairs(space_format) do
48-
field_names[field_format.name] = true
49-
end
50-
51-
local index_names = {}
52-
53-
-- If we use # (not table.maxn), we may lose indexes, when user drop some indexes.
54-
-- E.g: we have table with indexes id {1, 2, 3, nil, nil, 6}.
55-
-- If we use #{1, 2, 3, nil, nil, 6} (== 3) we will lose index with id = 6.
56-
-- See details: https://github.com/tarantool/crud/issues/103
57-
for i = 0, table.maxn(space_indexes) do
58-
local index = space_indexes[i]
59-
if index ~= nil then
60-
index_names[index.name] = true
61-
end
62-
end
63-
64-
for _, condition in ipairs(conditions) do
65-
if index_names[condition.operand] == nil and field_names[condition.operand] == nil then
66-
return false, ValidateConditionsError:new("No field or index %q found", condition.operand)
67-
end
68-
end
69-
70-
return true
71-
end
72-
7343
local function extract_sharding_key_from_scan_value(scan_value, scan_index, sharding_index)
7444
if #scan_value < #sharding_index.parts then
7545
return nil
@@ -157,11 +127,6 @@ function select_plan.new(space, conditions, opts)
157127
local space_indexes = space.index
158128
local space_format = space:format()
159129

160-
local ok, err = validate_conditions(conditions, space_indexes, space_format)
161-
if not ok then
162-
return nil, SelectPlanError:new('Passed bad conditions: %s', err)
163-
end
164-
165130
if conditions == nil then -- also cdata<NULL>
166131
conditions = {}
167132
end

test/entrypoint/srv_select.lua

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,31 @@ package.preload['customers-storage'] = function()
126126
unique = false,
127127
if_not_exists = true,
128128
})
129+
130+
local developers_space = box.schema.space.create('developers', {
131+
format = {
132+
{name = 'id', type = 'unsigned'},
133+
{name = 'bucket_id', type = 'unsigned'},
134+
{name = 'name', type = 'string'},
135+
{name = 'last_name', type = 'string'},
136+
{name = 'age', type = 'number'},
137+
{name = 'additional', type = 'any'},
138+
},
139+
if_not_exists = true,
140+
engine = engine,
141+
})
142+
143+
-- primary index
144+
developers_space:create_index('id_index', {
145+
parts = { 'id' },
146+
if_not_exists = true,
147+
})
148+
149+
developers_space:create_index('bucket_id', {
150+
parts = { 'bucket_id' },
151+
unique = false,
152+
if_not_exists = true,
153+
})
129154
end,
130155
}
131156
end

test/integration/select_test.lua

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pgroup:set_after_all(function(g) helpers.stop_cluster(g.cluster) end)
3131

3232
pgroup:set_before_each(function(g)
3333
helpers.truncate_space_on_cluster(g.cluster, 'customers')
34+
helpers.truncate_space_on_cluster(g.cluster, 'developers')
3435
end)
3536

3637

@@ -1199,3 +1200,57 @@ pgroup:add('test_select_force_map_call', function(g)
11991200
table.sort(objects, function(obj1, obj2) return obj1.bucket_id < obj2.bucket_id end)
12001201
t.assert_equals(objects, customers)
12011202
end)
1203+
1204+
pgroup:add('test_jsonpath', function(g)
1205+
helpers.insert_objects(g, 'developers', {
1206+
{
1207+
id = 1, name = "Alexey", last_name = "Smith",
1208+
age = 20, additional = { a = { b = 140 } },
1209+
}, {
1210+
id = 2, name = "Sergey", last_name = "Choppa",
1211+
age = 21, additional = { a = { b = 120 } },
1212+
}, {
1213+
id = 3, name = "Mikhail", last_name = "Crossman",
1214+
age = 42, additional = {},
1215+
}, {
1216+
id = 4, name = "Pavel", last_name = "White",
1217+
age = 51, additional = { a = { b = 50 } },
1218+
}, {
1219+
id = 5, name = "Tatyana", last_name = "May",
1220+
age = 17, additional = { a = 55 },
1221+
},
1222+
})
1223+
1224+
local result, err = g.cluster.main_server.net_box:call('crud.select',
1225+
{'developers', {{'>=', '[5]', 40}}, {fields = {'name', 'last_name'}}})
1226+
t.assert_equals(err, nil)
1227+
1228+
local objects = crud.unflatten_rows(result.rows, result.metadata)
1229+
local expected_objects = {
1230+
{id = 3, name = "Mikhail", last_name = "Crossman"},
1231+
{id = 4, name = "Pavel", last_name = "White"},
1232+
}
1233+
t.assert_equals(objects, expected_objects)
1234+
1235+
local result, err = g.cluster.main_server.net_box:call('crud.select',
1236+
{'developers', {{'<', '["age"]', 21}}, {fields = {'name', 'last_name'}}})
1237+
t.assert_equals(err, nil)
1238+
1239+
local objects = crud.unflatten_rows(result.rows, result.metadata)
1240+
local expected_objects = {
1241+
{id = 1, name = "Alexey", last_name = "Smith"},
1242+
{id = 5, name = "Tatyana", last_name = "May"},
1243+
}
1244+
t.assert_equals(objects, expected_objects)
1245+
1246+
local result, err = g.cluster.main_server.net_box:call('crud.select',
1247+
{'developers', {{'>=', '[6].a.b', 55}}, {fields = {'name', 'last_name'}}})
1248+
t.assert_equals(err, nil)
1249+
1250+
local objects = crud.unflatten_rows(result.rows, result.metadata)
1251+
local expected_objects = {
1252+
{id = 1, name = "Alexey", last_name = "Smith"},
1253+
{id = 2, name = "Sergey", last_name = "Choppa"},
1254+
}
1255+
t.assert_equals(objects, expected_objects)
1256+
end)

test/integration/simple_operations_test.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,3 +1006,4 @@ pgroup:add('test_partial_result_bad_input', function(g)
10061006
t.assert_equals(result, nil)
10071007
t.assert_str_contains(err.err, 'Space format doesn\'t contain field named "lastname"')
10081008
end)
1009+

0 commit comments

Comments
 (0)