Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit beeba2c

Browse files
committed
PCRE matching of string fields
Related to #73.
1 parent 6e2411b commit beeba2c

File tree

7 files changed

+349
-47
lines changed

7 files changed

+349
-47
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ make test
9999
* tarantool,
100100
* lulpeg,
101101
* >=tarantool/avro-schema-2.0-71-gfea0ead,
102-
* >=tarantool/shard-1.1-91-gfa88bf8 (optional).
102+
* >=tarantool/shard-1.1-91-gfa88bf8 (optional),
103+
* lrexlib-pcre (optional).
103104
* For test (additionally to 'for use'):
104105
* python 2.7,
105106
* virtualenv,

graphql/accessor_general.lua

+152-42
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ local json = require('json')
88
local avro_schema = require('avro_schema')
99
local utils = require('graphql.utils')
1010
local clock = require('clock')
11+
local rex = utils.optional_require('rex_pcre')
12+
13+
-- XXX: consider using [1] when it will be mature enough;
14+
-- look into [2] for the status.
15+
-- [1]: https://github.com/igormunkin/lua-re
16+
-- [2]: https://github.com/tarantool/tarantool/issues/2764
1117

1218
local accessor_general = {}
1319
local DEF_RESULTING_OBJECT_CNT_MAX = 10000
@@ -696,6 +702,36 @@ local function validate_collections(collections, schemas)
696702
end
697703
end
698704

705+
--- Whether an object match set of PCRE.
706+
---
707+
--- @tparam table obj an object to check
708+
---
709+
--- @tparam table pcre map with PCRE as values; names are correspond to field
710+
--- names of the `obj` to match
711+
---
712+
--- @treturn boolean `res` whether the `obj` object match `pcre` set of
713+
--- regexps.
714+
local function match_using_re(obj, pcre)
715+
if pcre == nil then return true end
716+
717+
for field_name, re in pairs(pcre) do
718+
-- skip an object with null in a string* field
719+
if obj[field_name] == nil then
720+
return false
721+
end
722+
assert(rex ~= nil, 'we should not pass over :compile() ' ..
723+
'with a query contains PCRE matching when there are '..
724+
'no lrexlib-pcre (rex_pcre) module present')
725+
-- XXX: compile re once
726+
local re = rex.new(re)
727+
if not re:match(obj[field_name]) then
728+
return false
729+
end
730+
end
731+
732+
return true
733+
end
734+
699735
--- Perform unflatten, skipping, filtering, limiting of objects. This is the
700736
--- core of the `select_internal` function.
701737
---
@@ -741,9 +777,11 @@ local function process_tuple(state, tuple, opts)
741777
qstats.fetched_object_cnt, fetched_object_cnt_max))
742778
assert(qcontext.deadline_clock > clock.monotonic64(),
743779
'query execution timeout exceeded, use `timeout_ms` to increase it')
780+
local collection_name = opts.collection_name
781+
local pcre = opts.pcre
744782

745783
-- convert tuple -> object
746-
local obj = opts.unflatten_tuple(opts.collection_name, tuple,
784+
local obj = opts.unflatten_tuple(collection_name, tuple,
747785
opts.default_unflatten_tuple)
748786

749787
-- skip all items before pivot (the item pointed by offset)
@@ -755,7 +793,8 @@ local function process_tuple(state, tuple, opts)
755793
end
756794

757795
-- filter out non-matching objects
758-
local match = utils.is_subtable(obj, filter)
796+
local match = utils.is_subtable(obj, filter) and
797+
match_using_re(obj, pcre)
759798
if do_filter then
760799
if not match then return true end
761800
else
@@ -828,6 +867,8 @@ local function select_internal(self, collection_name, from, filter, args, extra)
828867
-- XXX: save type at parsing and check here
829868
--assert(args.offset == nil or type(args.offset) == 'number',
830869
-- 'args.offset must be a number of nil, got ' .. type(args.offset))
870+
assert(args.pcre == nil or type(args.pcre) == 'table',
871+
'args.pcre must be nil or a table, got ' .. type(args.pcre))
831872

832873
local collection = self.collections[collection_name]
833874
assert(collection ~= nil,
@@ -876,6 +917,7 @@ local function select_internal(self, collection_name, from, filter, args, extra)
876917
collection_name = collection_name,
877918
unflatten_tuple = self.funcs.unflatten_tuple,
878919
default_unflatten_tuple = default_unflatten_tuple,
920+
pcre = args.pcre,
879921
}
880922

881923
if index == nil then
@@ -976,6 +1018,107 @@ local function init_qcontext(accessor, qcontext)
9761018
settings.timeout_ms * 1000 * 1000
9771019
end
9781020

1021+
--- Get an avro-schema for a primary key by a collection name.
1022+
---
1023+
--- @tparam table self accessor_general instance
1024+
---
1025+
--- @tparam string collection_name name of a collection
1026+
---
1027+
--- @treturn string `offset_type` is a just string in case of scalar primary
1028+
--- key (and, then, offset) type
1029+
---
1030+
--- @treturn table `offset_type` is a record in case of compound (multi-part)
1031+
--- primary key
1032+
local function get_primary_key_type(self, collection_name)
1033+
-- get name of field of primary key
1034+
local _, index_meta = get_primary_index_meta(
1035+
self, collection_name)
1036+
1037+
local collection = self.collections[collection_name]
1038+
local schema = self.schemas[collection.schema_name]
1039+
1040+
local offset_fields = {}
1041+
1042+
for _, field_name in ipairs(index_meta.fields) do
1043+
local field_type
1044+
for _, field in ipairs(schema.fields) do
1045+
if field.name == field_name then
1046+
field_type = field.type
1047+
end
1048+
end
1049+
assert(field_type ~= nil,
1050+
('cannot find type for primary index field "%s" ' ..
1051+
'for collection "%s"'):format(field_name,
1052+
collection_name))
1053+
assert(type(field_type) == 'string',
1054+
'field type must be a string, got ' ..
1055+
type(field_type))
1056+
offset_fields[#offset_fields + 1] = {
1057+
name = field_name,
1058+
type = field_type,
1059+
}
1060+
end
1061+
1062+
local offset_type
1063+
assert(#offset_fields > 0,
1064+
'offset must contain at least one field')
1065+
if #offset_fields == 1 then
1066+
-- use a scalar type
1067+
offset_type = offset_fields[1].type
1068+
else
1069+
-- construct an input type
1070+
offset_type = {
1071+
name = collection_name .. '_offset',
1072+
type = 'record',
1073+
fields = offset_fields,
1074+
}
1075+
end
1076+
1077+
return offset_type
1078+
end
1079+
1080+
-- XXX: add string fields of a nested record / 1:1 connection to
1081+
-- get_pcre_argument_type
1082+
1083+
--- Get an avro-schema for a pcre argument by a collection name.
1084+
---
1085+
--- Note: it is called from `list_args`, so applicable only for lists:
1086+
--- top-level objects and 1:N connections.
1087+
---
1088+
--- @tparam table self accessor_general instance
1089+
---
1090+
--- @tparam string collection_name name of a collection
1091+
---
1092+
--- @treturn table `pcre_type` is a record with fields per string/string* field
1093+
--- of an object of the collection
1094+
local function get_pcre_argument_type(self, collection_name)
1095+
local collection = self.collections[collection_name]
1096+
assert(collection ~= nil, 'cannot found collection ' ..
1097+
tostring(collection_name))
1098+
local schema = self.schemas[collection.schema_name]
1099+
assert(schema ~= nil, 'cannot found schema ' ..
1100+
tostring(collection.schema_name))
1101+
1102+
assert(schema.type == 'record',
1103+
'top-level object expected to be a record, got ' ..
1104+
tostring(schema.type))
1105+
1106+
local string_fields = {}
1107+
1108+
for _, field in ipairs(schema.fields) do
1109+
if field.type == 'string' or field.type == 'string*' then
1110+
string_fields[#string_fields + 1] = table.copy(field)
1111+
end
1112+
end
1113+
1114+
local pcre_type = {
1115+
name = collection_name .. '_pcre',
1116+
type = 'record',
1117+
fields = string_fields,
1118+
}
1119+
return pcre_type
1120+
end
1121+
9791122
--- Create a new data accessor.
9801123
---
9811124
--- Provided `funcs` argument determines certain functions for retrieving
@@ -1114,53 +1257,20 @@ function accessor_general.new(opts, funcs)
11141257
args, extra)
11151258
end,
11161259
list_args = function(self, collection_name)
1117-
-- get name of field of primary key
1118-
local _, index_meta = get_primary_index_meta(
1119-
self, collection_name)
1120-
1121-
local offset_fields = {}
1122-
1123-
for _, field_name in ipairs(index_meta.fields) do
1124-
local field_type
1125-
local collection = self.collections[collection_name]
1126-
local schema = self.schemas[collection.schema_name]
1127-
for _, field in ipairs(schema.fields) do
1128-
if field.name == field_name then
1129-
field_type = field.type
1130-
end
1131-
end
1132-
assert(field_type ~= nil,
1133-
('cannot find type for primary index field "%s" ' ..
1134-
'for collection "%s"'):format(field_name,
1135-
collection_name))
1136-
assert(type(field_type) == 'string',
1137-
'field type must be a string, got ' ..
1138-
type(field_type))
1139-
offset_fields[#offset_fields + 1] = {
1140-
name = field_name,
1141-
type = field_type,
1142-
}
1143-
end
1260+
local offset_type = get_primary_key_type(self, collection_name)
11441261

1145-
local offset_type
1146-
assert(#offset_fields > 0,
1147-
'offset must contain at least one field')
1148-
if #offset_fields == 1 then
1149-
-- use a scalar type
1150-
offset_type = offset_fields[1].type
1151-
else
1152-
-- construct an input type
1153-
offset_type = {
1154-
name = collection_name .. '_offset',
1155-
type = 'record',
1156-
fields = offset_fields,
1157-
}
1262+
-- add `pcre` argument only if lrexlib-pcre was found
1263+
local pcre_field
1264+
if rex ~= nil then
1265+
local pcre_type = get_pcre_argument_type(self, collection_name)
1266+
pcre_field = {name = 'pcre', type = pcre_type}
11581267
end
11591268

11601269
return {
11611270
{name = 'limit', type = 'int'},
11621271
{name = 'offset', type = offset_type},
11631272
-- {name = 'filter', type = ...},
1273+
pcre_field,
11641274
}
11651275
end,
11661276
}

graphql/tarantool_graphql.lua

+2-1
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,8 @@ end
729729
--- list_args = function(self, collection_name)
730730
--- return {
731731
--- {name = 'limit', type = 'int'},
732-
--- {name = 'offset', type = <...>}, -- type of primary key
732+
--- {name = 'offset', type = <...>}, -- type of a primary key
733+
--- {name = 'pcre', type = <...>},
733734
--- }
734735
--- end,
735736
--- }

graphql/utils.lua

+15
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,19 @@ function utils.gen_booking_table(data)
132132
})
133133
end
134134

135+
--- Catch error at module require and return nil in the case.
136+
---
137+
--- @tparam string module_name mane of a module to require
138+
---
139+
--- @return `module` or `nil`
140+
function utils.optional_require(module_name)
141+
assert(type(module_name) == 'string',
142+
'module_name must be a string, got ' .. type(module_name))
143+
local ok, module = pcall(require, module_name)
144+
if not ok then
145+
log.warn('optional_require: no module ' .. module_name)
146+
end
147+
return ok and module or nil
148+
end
149+
135150
return utils

test/local/space_pcre.result

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
RUN 1_1 {{{
2+
QUERY
3+
query users($offset: String, $first_name_re: String,
4+
$middle_name_re: String) {
5+
user_collection(pcre: {first_name: $first_name_re,
6+
middle_name: $middle_name_re}, offset: $offset) {
7+
first_name
8+
middle_name
9+
last_name
10+
}
11+
}
12+
VARIABLES
13+
---
14+
middle_name_re: ich$
15+
first_name_re: ^I
16+
...
17+
18+
RESULT
19+
---
20+
user_collection:
21+
- last_name: Ivanov
22+
first_name: Ivan
23+
middle_name: Ivanovich
24+
...
25+
26+
}}}
27+
28+
RUN 1_2 {{{
29+
QUERY
30+
query users($offset: String, $first_name_re: String,
31+
$middle_name_re: String) {
32+
user_collection(pcre: {first_name: $first_name_re,
33+
middle_name: $middle_name_re}, offset: $offset) {
34+
first_name
35+
middle_name
36+
last_name
37+
}
38+
}
39+
VARIABLES
40+
---
41+
user_id: user_id_1
42+
first_name_re: ^V
43+
...
44+
45+
RESULT
46+
---
47+
user_collection:
48+
- last_name: Pupkin
49+
first_name: Vasiliy
50+
...
51+
52+
}}}
53+

0 commit comments

Comments
 (0)