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

Commit 3b55445

Browse files
committed
Verify 1:1 connection use all fields of an index
We already check that the index is unique. The tuples given by a connection cannot be proven unique unless we match the tuples using *full* list of key parts of an unique index.
1 parent 3fff437 commit 3b55445

5 files changed

+307
-192
lines changed

graphql/accessor_general.lua

+36-31
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,10 @@ end
405405
---
406406
--- @tparam table indexes map from collection names to indexes meta-information
407407
--- as defined in the @{new} function; the function uses it to validate index
408-
--- names provided in connections (which are inside collections) and validate
408+
--- names provided in connections (which are inside collections), validate
409409
--- connection types ('1:1' or '1:N') against index uniqueness if the `unique`
410-
--- flag provided for corresponding index
410+
--- flag provided for corresponding index and to check that destination parts
411+
--- of connections form a prefix of parts of the `connection.index_name` index
411412
---
412413
--- @tparam table collections map from collection names to collections as
413414
--- defined in the @{accessor_general.new} function decription; the function
@@ -431,16 +432,41 @@ local function build_connection_indexes(indexes, collections)
431432
'index_name must be a string, got ' .. type(index_name))
432433

433434
-- validate index_name against 'indexes'
434-
local index = indexes[c.destination_collection]
435-
assert(type(index) == 'table',
436-
'index must be a table, got ' .. type(index))
435+
local index_meta = indexes[c.destination_collection]
436+
assert(type(index_meta) == 'table',
437+
'index_meta must be a table, got ' .. type(index_meta))
437438

438-
-- XXX: validate connection parts are match or being prefix of
439-
-- index fields
439+
-- validate connection parts are match or being prefix of index
440+
-- fields
441+
local i = 1
442+
local index_fields = index_meta[c.index_name].fields
443+
for _, part in ipairs(c.parts) do
444+
assert(type(part.source_field) == 'string',
445+
'part.source_field must be a string, got ' ..
446+
type(part.source_field))
447+
assert(type(part.destination_field) == 'string',
448+
'part.destination_field must be a string, got ' ..
449+
type(part.destination_field))
450+
assert(part.destination_field == index_fields[i],
451+
('connection "%s" of collection "%s" ' ..
452+
'has destination parts that is not prefix of the index ' ..
453+
'"%s" parts'):format(c.name, c.destination_collection,
454+
c.index_name))
455+
i = i + 1
456+
end
457+
local parts_cnt = i - 1
458+
459+
-- partial index of an unique index is not guaranteed to being
460+
-- unique
461+
assert(c.type == '1:N' or parts_cnt == #index_fields,
462+
('1:1 connection "%s" of collection "%s" ' ..
463+
'has less fields than the index "%s" has (cannot prove ' ..
464+
'uniqueness of the partial index)'):format(c.name,
465+
c.destination_collection, c.index_name))
440466

441467
-- validate connection type against index uniqueness (if provided)
442-
if index.unique ~= nil then
443-
assert(c.type == '1:N' or index.unique == true,
468+
if index_meta.unique ~= nil then
469+
assert(c.type == '1:N' or index_meta.unique == true,
444470
('1:1 connection ("%s") cannot be implemented ' ..
445471
'on top of non-unique index ("%s")'):format(
446472
c.name, index_name))
@@ -478,13 +504,8 @@ end
478504
--- @{tarantool_graphql.new} function; this is for validate collection against
479505
--- certain set of schemas (no 'dangling' schema names in collections)
480506
---
481-
--- @tparam table indexes map from collection names to indexes meta-information
482-
--- as defined in the @{new} function; the function uses it to check that
483-
--- destination parts of connections form a prefix of parts of the
484-
--- `connection.index_name` index
485-
---
486507
--- @return nil
487-
local function validate_collections(collections, schemas, indexes)
508+
local function validate_collections(collections, schemas)
488509
for collection_name, collection in pairs(collections) do
489510
assert(type(collection_name) == 'string',
490511
'collection_name must be a string, got ' ..
@@ -517,22 +538,6 @@ local function validate_collections(collections, schemas, indexes)
517538
assert(type(connection.index_name) == 'string',
518539
'connection.index_name must be a string, got ' ..
519540
type(connection.index_name))
520-
local i = 1
521-
for _, part in ipairs(connection.parts) do
522-
assert(type(part.source_field) == 'string',
523-
'part.source_field must be a string, got ' ..
524-
type(part.source_field))
525-
assert(type(part.destination_field) == 'string',
526-
'part.destination_field must be a string, got ' ..
527-
type(part.destination_field))
528-
assert(part.destination_field ==
529-
indexes[collection_name][connection.index_name].fields[i],
530-
('connection "%s" of collection "%s" ' ..
531-
'has destination parts that is not prefix of the index ' ..
532-
'"%s" parts'):format(connection.name, collection_name,
533-
connection.index_name))
534-
i = i + 1
535-
end
536541
end
537542
end
538543
end

test/local/init_fail.result

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
INIT: ok: false; err: 1:1 connection "user_connection" of collection "user_collection" has less fields than the index "user_str_num_index" has (cannot prove uniqueness of the partial index)
2+
INIT: ok: true; type(res): table

test/local/init_fail.test.lua

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env tarantool
2+
3+
local fio = require('fio')
4+
5+
-- require in-repo version of graphql/ sources despite current working directory
6+
package.path = fio.abspath(debug.getinfo(1).source:match("@?(.*/)")
7+
:gsub('/./', '/'):gsub('/+$', '')) .. '/../../?.lua' .. ';' ..
8+
package.path
9+
10+
local graphql = require('graphql')
11+
local testdata = require('test.local.space_compound_index_testdata')
12+
13+
-- utils
14+
-- -----
15+
16+
-- return an error w/o file name and line number
17+
local function strip_error(err)
18+
return tostring(err):gsub('^.-:.-: (.*)$', '%1')
19+
end
20+
21+
-- init box, upload test data and acquire metadata
22+
-- -----------------------------------------------
23+
24+
-- init box and data schema
25+
testdata.init_spaces()
26+
27+
-- upload test data
28+
testdata.fill_test_data()
29+
30+
-- acquire metadata
31+
local metadata = testdata.get_test_metadata()
32+
33+
-- inject an error into the metadata
34+
-- ---------------------------------
35+
36+
local saved_part =
37+
metadata.collections.order_collection.connections[1].parts[2]
38+
metadata.collections.order_collection.connections[1].parts[2] = nil
39+
40+
-- build accessor and graphql schemas
41+
-- ----------------------------------
42+
43+
local function create_gql_wrapper(metadata)
44+
local accessor = graphql.accessor_space.new({
45+
schemas = metadata.schemas,
46+
collections = metadata.collections,
47+
service_fields = metadata.service_fields,
48+
indexes = metadata.indexes,
49+
})
50+
51+
return graphql.new({
52+
schemas = metadata.schemas,
53+
collections = metadata.collections,
54+
accessor = accessor,
55+
})
56+
57+
end
58+
59+
local ok, err = pcall(create_gql_wrapper, metadata)
60+
print(('INIT: ok: %s; err: %s'):format(tostring(ok), strip_error(err)))
61+
62+
-- restore back cut part
63+
metadata.collections.order_collection.connections[1].parts[2] = saved_part
64+
65+
local ok, res = pcall(create_gql_wrapper, metadata)
66+
print(('INIT: ok: %s; type(res): %s'):format(tostring(ok), type(res)))
67+
68+
-- clean up
69+
-- --------
70+
71+
testdata.drop()
72+
73+
os.exit()

test/local/space_compound_index.test.lua

+16-161
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ package.path = fio.abspath(debug.getinfo(1).source:match("@?(.*/)")
77
:gsub('/./', '/'):gsub('/+$', '')) .. '/../../?.lua' .. ';' ..
88
package.path
99

10-
local json = require('json')
1110
local yaml = require('yaml')
1211
local graphql = require('graphql')
1312
local utils = require('graphql.utils')
13+
local testdata = require('test.local.space_compound_index_testdata')
1414

1515
-- utils
1616
-- -----
@@ -20,164 +20,21 @@ local function strip_error(err)
2020
return tostring(err):gsub('^.-:.-: (.*)$', '%1')
2121
end
2222

23-
-- schemas and meta-information
24-
-- ----------------------------
25-
26-
local schemas = json.decode([[{
27-
"user": {
28-
"type": "record",
29-
"name": "user",
30-
"fields": [
31-
{ "name": "user_str", "type": "string" },
32-
{ "name": "user_num", "type": "long" },
33-
{ "name": "first_name", "type": "string" },
34-
{ "name": "last_name", "type": "string" }
35-
]
36-
},
37-
"order": {
38-
"type": "record",
39-
"name": "order",
40-
"fields": [
41-
{ "name": "order_str", "type": "string" },
42-
{ "name": "order_num", "type": "long" },
43-
{ "name": "user_str", "type": "string" },
44-
{ "name": "user_num", "type": "long" },
45-
{ "name": "description", "type": "string" }
46-
]
47-
}
48-
}]])
49-
50-
local collections = json.decode([[{
51-
"user_collection": {
52-
"schema_name": "user",
53-
"connections": [
54-
{
55-
"type": "1:N",
56-
"name": "order_connection",
57-
"destination_collection": "order_collection",
58-
"parts": [
59-
{ "source_field": "user_str", "destination_field": "user_str" },
60-
{ "source_field": "user_num", "destination_field": "user_num" }
61-
],
62-
"index_name": "user_str_num_index"
63-
},
64-
{
65-
"type": "1:N",
66-
"name": "order_str_connection",
67-
"destination_collection": "order_collection",
68-
"parts": [
69-
{ "source_field": "user_str", "destination_field": "user_str" }
70-
],
71-
"index_name": "user_str_num_index"
72-
}
73-
]
74-
},
75-
"order_collection": {
76-
"schema_name": "order",
77-
"connections": [
78-
{
79-
"type": "1:1",
80-
"name": "user_connection",
81-
"destination_collection": "user_collection",
82-
"parts": [
83-
{ "source_field": "user_str", "destination_field": "user_str" },
84-
{ "source_field": "user_num", "destination_field": "user_num" }
85-
],
86-
"index_name": "user_str_num_index"
87-
}
88-
]
89-
}
90-
}]])
91-
92-
local service_fields = {
93-
user = {},
94-
order = {},
95-
}
96-
97-
local indexes = {
98-
user_collection = {
99-
user_str_num_index = {
100-
service_fields = {},
101-
fields = {'user_str', 'user_num'},
102-
index_type = 'tree',
103-
unique = true,
104-
primary = true,
105-
},
106-
},
107-
order_collection = {
108-
order_str_num_index = {
109-
service_fields = {},
110-
fields = {'order_str', 'order_num'},
111-
index_type = 'tree',
112-
unique = true,
113-
primary = true,
114-
},
115-
user_str_num_index = {
116-
service_fields = {},
117-
fields = {'user_str', 'user_num'},
118-
index_type = 'tree',
119-
unique = false,
120-
primary = false,
121-
},
122-
},
123-
}
124-
125-
-- fill spaces
126-
-- -----------
127-
128-
-- user_collection fields
129-
local U_USER_STR_FN = 1
130-
local U_USER_NUM_FN = 2
131-
132-
-- order_collection fields
133-
local O_ORDER_STR_FN = 1
134-
local O_ORDER_NUM_FN = 2
135-
local O_USER_STR_FN = 3
136-
local O_USER_NUM_FN = 4
137-
138-
box.cfg{background = false}
139-
box.once('test_space_init_spaces', function()
140-
-- users
141-
box.schema.create_space('user_collection')
142-
box.space.user_collection:create_index('user_str_num_index',
143-
{type = 'tree', unique = true, parts = {
144-
U_USER_STR_FN, 'string', U_USER_NUM_FN, 'unsigned',
145-
}}
146-
)
147-
148-
-- orders
149-
box.schema.create_space('order_collection')
150-
box.space.order_collection:create_index('order_str_num_index',
151-
{type = 'tree', unique = true, parts = {
152-
O_ORDER_STR_FN, 'string', O_ORDER_NUM_FN, 'unsigned',
153-
}}
154-
)
155-
box.space.order_collection:create_index('user_str_num_index',
156-
{type = 'tree', unique = false, parts = {
157-
O_USER_STR_FN, 'string', O_USER_NUM_FN, 'unsigned',
158-
}}
159-
)
160-
end)
23+
-- init box, upload test data and acquire metadata
24+
-- -----------------------------------------------
16125

162-
for i = 1, 20 do
163-
for j = 1, 5 do
164-
local s =
165-
j % 5 == 1 and 'a' or
166-
j % 5 == 2 and 'b' or
167-
j % 5 == 3 and 'c' or
168-
j % 5 == 4 and 'd' or
169-
j % 5 == 0 and 'e' or
170-
nil
171-
assert(s ~= nil, 's must not be nil')
172-
box.space.user_collection:replace(
173-
{'user_str_' .. s, i, 'first name ' .. s, 'last name ' .. s})
174-
for k = 1, 10 do
175-
box.space.order_collection:replace(
176-
{'order_str_' .. s .. '_' .. tostring(k), i * 100 + k,
177-
'user_str_' .. s, i, 'description ' .. s})
178-
end
179-
end
180-
end
26+
-- init box and data schema
27+
testdata.init_spaces()
28+
29+
-- upload test data
30+
testdata.fill_test_data()
31+
32+
-- acquire metadata
33+
local metadata = testdata.get_test_metadata()
34+
local schemas = metadata.schemas
35+
local collections = metadata.collections
36+
local service_fields = metadata.service_fields
37+
local indexes = metadata.indexes
18138

18239
-- build accessor and graphql schemas
18340
-- ----------------------------------
@@ -381,8 +238,6 @@ print(('RESULT: ok: %s; err: %s'):format(tostring(ok), strip_error(err)))
381238
-- clean up
382239
-- --------
383240

384-
box.space._schema:delete('oncetest_space_init_spaces')
385-
box.space.user_collection:drop()
386-
box.space.order_collection:drop()
241+
testdata.drop()
387242

388243
os.exit()

0 commit comments

Comments
 (0)