diff --git a/graphql/tarantool_graphql.lua b/graphql/tarantool_graphql.lua index 70bccd2..692bf34 100644 --- a/graphql/tarantool_graphql.lua +++ b/graphql/tarantool_graphql.lua @@ -56,38 +56,62 @@ local default_instance -- forward declarations local gql_type -local function avro_type(avro_schema) +local function is_scalar_type(avro_schema_type) + check(avro_schema_type, 'avro_schema_type', 'string') + + local scalar_types = { + ['int'] = true, + ['int*'] = true, + ['long'] = true, + ['long*'] = true, +--[[ + ['float'] = true, + ['float*'] = true, + ['double'] = true, + ['double*'] = true, + ['boolean'] = true, + ['boolean*'] = true, +]]-- + ['string'] = true, + ['string*'] = true, + ['null'] = true, + } + + return scalar_types[avro_schema_type] or false +end + +local function is_compound_type(avro_schema_type) + check(avro_schema_type, 'avro_schema_type', 'string') + + local compound_types = { + ['record'] = true, + ['record*'] = true, + ['array'] = true, + ['array*'] = true, + ['map'] = true, + ['map*'] = true, + } + + return compound_types[avro_schema_type] or false +end + +local function avro_type(avro_schema, opts) + local opts = opts or {} + local allow_references = opts.allow_references or false + if type(avro_schema) == 'table' then - if avro_schema.type == 'record' then - return 'record' - elseif avro_schema.type == 'record*' then - return 'record*' - elseif utils.is_array(avro_schema) then + if utils.is_array(avro_schema) then return 'union' - elseif avro_schema.type == 'array' then - return 'array' - elseif avro_schema.type == 'array*' then - return 'array*' - elseif avro_schema.type == 'map' then - return 'map' - elseif avro_schema.type == 'map*' then - return 'map*' + elseif is_compound_type(avro_schema.type) then + return avro_schema.type + elseif allow_references then + return avro_schema end elseif type(avro_schema) == 'string' then - if avro_schema == 'int' then - return 'int' - elseif avro_schema == 'int*' then - return 'int*' - elseif avro_schema == 'long' then - return 'long' - elseif avro_schema == 'long*' then - return 'long*' - elseif avro_schema == 'string' then - return 'string' - elseif avro_schema == 'string*' then - return 'string*' - elseif avro_schema == 'null' then - return 'null' + if is_scalar_type(avro_schema) then + return avro_schema + elseif allow_references then + return avro_schema end end error('unrecognized avro-schema type: ' .. json.encode(avro_schema)) @@ -247,17 +271,14 @@ local function convert_record_fields_to_args(fields, opts) assert(type(field.name) == 'string', ('field.name must be a string, got %s (schema %s)') :format(type(field.name), json.encode(field))) - - -- records, arrays (gql lists) and maps can't be arguments, so these - -- graphql types are to be skipped - local avro_t = avro_type(field.type) - if not skip_compound or ( - avro_t ~= 'record' and avro_t ~= 'record*' and - avro_t ~= 'array' and avro_t ~= 'array*' and - avro_t ~= 'map' and avro_t ~= 'map*' and - avro_t ~= 'union') then - - local gql_class = gql_argument_type(field.type, field.name) + -- records, arrays (gql lists), maps and unions can't be arguments, so + -- these graphql types are to be skipped; + -- skip_compound == false is the trick for accessor_general-provided + -- record; we don't expect map, array or union here as well as we don't + -- expect avro-schema reference. + local avro_t = avro_type(field.type, {allow_references = true}) + if not skip_compound or is_scalar_type(avro_t) then + local gql_class = gql_argument_type(field.type) args[field.name] = nullable(gql_class) end end @@ -925,7 +946,7 @@ gql_type = function(state, avro_schema, collection, collection_name, field_name) 'state.accessor.list_args must not be nil') -- type of the top element in the avro-schema - local avro_t = avro_type(avro_schema) + local avro_t = avro_type(avro_schema, {allow_references = true}) if avro_t == 'record' or avro_t == 'record*' then assert(type(avro_schema.name) == 'string', @@ -949,6 +970,11 @@ gql_type = function(state, avro_schema, collection, collection_name, field_name) avro_schema.name, fields = fields, }) + assert(state.definitions[avro_schema.name] == nil and + state.definitions[avro_schema.name .. '*'] == nil, + 'multiple definitions of ' .. avro_schema.name) + state.definitions[avro_schema.name] = types.nonNull(res) + state.definitions[avro_schema.name .. '*'] = res return avro_t == 'record' and types.nonNull(res) or res elseif avro_t == 'enum' then error('enums not implemented yet') -- XXX @@ -980,6 +1006,12 @@ gql_type = function(state, avro_schema, collection, collection_name, field_name) elseif avro_t == 'union' then return create_gql_union(state, avro_schema, field_name) else + if type(avro_schema) == 'string' then + if state.definitions[avro_schema] ~= nil then + return state.definitions[avro_schema] + end + end + local res = convert_scalar_type(avro_schema, {raise = false}) if res == nil then error('unrecognized avro-schema type: ' .. @@ -1119,6 +1151,9 @@ local function parse_cfg(cfg) state.list_arguments = utils.gen_booking_table({}) state.all_arguments = utils.gen_booking_table({}) + -- map from avro-schema names to graphql types + state.definitions = {} + local accessor = cfg.accessor assert(accessor ~= nil, 'cfg.accessor must not be nil') assert(accessor.select ~= nil, 'cfg.accessor.select must not be nil') diff --git a/test/common/avro_refs.test.lua b/test/common/avro_refs.test.lua new file mode 100755 index 0000000..d3fc82e --- /dev/null +++ b/test/common/avro_refs.test.lua @@ -0,0 +1,16 @@ +#!/usr/bin/env tarantool + +local fio = require('fio') + +-- require in-repo version of graphql/ sources despite current working directory +package.path = fio.abspath(debug.getinfo(1).source:match("@?(.*/)") + :gsub('/./', '/'):gsub('/+$', '')) .. '/../../?.lua' .. ';' .. package.path + +local utils = require('test.utils') +local testdata = require('test.common.lua.test_data_avro_refs') + +box.cfg({}) + +utils.run_testdata(testdata) + +os.exit() diff --git a/test/common/lua/multirunner.lua b/test/common/lua/multirunner.lua index cebf67c..d46f2b9 100755 --- a/test/common/lua/multirunner.lua +++ b/test/common/lua/multirunner.lua @@ -165,8 +165,11 @@ end local function run(test_run, init_function, cleanup_function, workload) -- ensure stable order local names = {} - for conf_name, _ in pairs(CONFS) do - names[#names + 1] = conf_name + for conf_name, conf in pairs(CONFS) do + -- allow to run w/o test-run + if test_run ~= nil or conf.type == 'space' then + names[#names + 1] = conf_name + end end table.sort(names) diff --git a/test/common/lua/test_data_avro_refs.lua b/test/common/lua/test_data_avro_refs.lua new file mode 100644 index 0000000..f1cd3eb --- /dev/null +++ b/test/common/lua/test_data_avro_refs.lua @@ -0,0 +1,214 @@ +-- Avro-schema references +-- https://github.com/tarantool/graphql/issues/116 + +-- XXX: check 'fixed' type when we'll support it +-- XXX: check 'enum' type when we'll support it + +local tap = require('tap') +local json = require('json') +local yaml = require('yaml') +local utils = require('graphql.utils') + +local testdata = {} + +testdata.meta = { + schemas = json.decode([[{ + "foo": { + "type": "record", + "name": "foo", + "fields": [ + {"name": "id", "type": "long"}, + { + "name": "bar", + "type": { + "type": "record", + "name": "bar", + "fields": [ + {"name": "x", "type": "long"}, + {"name": "y", "type": "long"} + ] + } + }, + { + "name": "bar_ref", + "type": "bar" + }, + { + "name": "bar_nref", + "type": "bar*" + }, + { + "name": "baz", + "type": { + "type": "record*", + "name": "baz", + "fields": [ + {"name": "x", "type": "long"}, + {"name": "y", "type": "long"} + ] + } + }, + { + "name": "baz_ref", + "type": "baz" + }, + { + "name": "baz_nref", + "type": "baz*" + } + ] + } + }]]), + collections = json.decode([[{ + "foo": { + "schema_name": "foo", + "connections": [] + } + }]]), + service_fields = { + foo = {}, + }, + indexes = { + foo = { + id = { + service_fields = {}, + fields = {'id'}, + index_type = 'tree', + unique = true, + primary = true, + }, + }, + } +} + +function testdata.init_spaces() + -- foo fields + local ID_FN = 1 + + box.schema.create_space('foo') + box.space.foo:create_index('id', { + type = 'tree', unique = true, parts = {ID_FN, 'unsigned'}}) +end + +function testdata.drop_spaces() + box.space.foo:drop() +end + +function testdata.fill_test_data(virtbox) + local NULL_T = 0 + local VALUE_T = 1 + + local x = 1000 + local y = 2000 + local a = 3000 + local b = 4000 + + -- non-null bar, baz and its refs + virtbox.foo:replace({1, -- id + x, y, x, y, VALUE_T, {x, y}, -- bar & refs + VALUE_T, {a, b}, a, b, VALUE_T, {a, b}, -- baz & refs + }) + -- null in nullable bar, baz refs + virtbox.foo:replace({2, -- id + x, y, x, y, NULL_T, box.NULL, -- bar & refs + NULL_T, box.NULL, a, b, NULL_T, box.NULL, -- baz & refs + }) +end + +function testdata.run_queries(gql_wrapper) + local test = tap.test('avro_refs') + test:plan(2) + + local query_1 = [[ + query get_by_id($id: Long) { + foo(id: $id) { + id + bar { + x + y + } + bar_ref { + x + y + } + bar_nref { + x + y + } + baz { + x + y + } + baz_ref { + x + y + } + baz_nref { + x + y + } + } + } + ]] + + local gql_query_1 = utils.show_trace(function() + return gql_wrapper:compile(query_1) + end) + + local variables_1_1 = {id = 1} + local result_1_1 = utils.show_trace(function() + return gql_query_1:execute(variables_1_1) + end) + + local exp_result_1_1 = yaml.decode(([[ + --- + foo: + - id: 1 + bar: + x: 1000 + y: 2000 + bar_ref: + x: 1000 + y: 2000 + bar_nref: + x: 1000 + y: 2000 + baz: + x: 3000 + y: 4000 + baz_ref: + x: 3000 + y: 4000 + baz_nref: + x: 3000 + y: 4000 + ]]):strip()) + + test:is_deeply(result_1_1, exp_result_1_1, '1_1') + + local variables_1_2 = {id = 2} + local result_1_2 = utils.show_trace(function() + return gql_query_1:execute(variables_1_2) + end) + + local exp_result_1_2 = yaml.decode(([[ + --- + foo: + - id: 2 + bar: + x: 1000 + y: 2000 + bar_ref: + x: 1000 + y: 2000 + baz_ref: + x: 3000 + y: 4000 + ]]):strip()) + + test:is_deeply(result_1_2, exp_result_1_2, '1_2') + + assert(test:check(), 'check plan') +end + +return testdata diff --git a/test/common/lua/test_data_nested_record.lua b/test/common/lua/test_data_nested_record.lua index 31c82ed..335708b 100644 --- a/test/common/lua/test_data_nested_record.lua +++ b/test/common/lua/test_data_nested_record.lua @@ -27,7 +27,7 @@ testdata.meta = { {"name": "y", "type": "long"} ] } - } + } ] } }]]), diff --git a/test/common/nested_record.test.lua b/test/common/nested_record.test.lua index 1fbdfea..3a7a122 100755 --- a/test/common/nested_record.test.lua +++ b/test/common/nested_record.test.lua @@ -1,35 +1,33 @@ #!/usr/bin/env tarantool local fio = require('fio') -local multirunner = require('multirunner') -local testdata = require('test_data_nested_record') -local test_run = require('test_run').new() -local graphql = require('graphql') - -box.cfg({}) -- require in-repo version of graphql/ sources despite current working directory package.path = fio.abspath(debug.getinfo(1).source:match("@?(.*/)") :gsub('/./', '/'):gsub('/+$', '')) .. '/../../?.lua' .. ';' .. package.path +local multirunner = require('test.common.lua.multirunner') +local testdata = require('test.common.lua.test_data_nested_record') +local graphql = require('graphql') +local utils = require('graphql.utils') +local test_run = utils.optional_require('test_run') +if test_run then + test_run = test_run.new() +end + +box.cfg({}) + local function run(setup_name, shard) print(setup_name) - local accessor_class = shard and graphql.accessor_shard or - graphql.accessor_space local virtbox = shard or box.space - local accessor = accessor_class.new({ + local gql_wrapper = graphql.new({ schemas = testdata.meta.schemas, collections = testdata.meta.collections, service_fields = testdata.meta.service_fields, indexes = testdata.meta.indexes, - }) - - local gql_wrapper = graphql.new({ - schemas = testdata.meta.schemas, - collections = testdata.meta.collections, - accessor = accessor, + accessor = shard and 'shard' or 'space', }) testdata.fill_test_data(virtbox) diff --git a/test/common/suite.cfg b/test/common/suite.cfg new file mode 100644 index 0000000..2e2aa8c --- /dev/null +++ b/test/common/suite.cfg @@ -0,0 +1,7 @@ +{ + "avro_refs.test.lua": { + "space": {"conf": "space"}, + "shard_2x2": {"conf": "shard_2x2"}, + "shard_4x1": {"conf": "shard_4x1"} + } +} diff --git a/test/common/suite.ini b/test/common/suite.ini index cbc146d..922bda9 100644 --- a/test/common/suite.ini +++ b/test/common/suite.ini @@ -1,6 +1,6 @@ [default] core = app description = tests different setups simultaneously -lua_libs = lua/multirunner.lua lua/test_data_user_order.lua \ - lua/test_data_nested_record.lua +lua_libs = lua/multirunner.lua lua/test_data_user_order.lua +config = suite.cfg is_parallel = True diff --git a/test/utils.lua b/test/utils.lua index b045d4c..cbbb5a7 100644 --- a/test/utils.lua +++ b/test/utils.lua @@ -1,6 +1,17 @@ --- Various utility function used across the graphql module tests. +local fio = require('fio') + +-- require in-repo version of graphql/ sources despite current working directory +package.path = fio.abspath(debug.getinfo(1).source:match("@?(.*/)") + :gsub('/./', '/'):gsub('/+$', '')) .. '/../?.lua' .. ';' .. package.path + local yaml = require('yaml') +local graphql = require('graphql') +local multirunner = require('test.common.lua.multirunner') +local graphql_utils = require('graphql.utils') +local test_run = graphql_utils.optional_require('test_run') +test_run = test_run and test_run.new() local utils = {} @@ -14,4 +25,40 @@ function utils.print_and_return(...) return table.concat({ ... }, ' ') .. '\n' end +function utils.graphql_from_testdata(testdata, shard) + local meta = testdata.meta or testdata.get_test_metadata() + + local gql_wrapper = graphql.new({ + schemas = meta.schemas, + collections = meta.collections, + service_fields = meta.service_fields, + indexes = meta.indexes, + accessor = shard and 'shard' or 'space', + }) + + return gql_wrapper +end + +function utils.run_testdata(testdata, opts) + local opts = opts or {} + local run_queries = opts.run_queries or testdata.run_queries + + -- allow to run under tarantool on 'space' configuration w/o test-run + local conf_name = test_run and test_run:get_cfg('conf') or 'space' + + multirunner.run_conf(conf_name, { + test_run = test_run, + init_function = testdata.init_spaces, + cleanup_function = testdata.drop_spaces, + workload = function(_, shard) + local virtbox = shard or box.space + testdata.fill_test_data(virtbox) + local gql_wrapper = utils.graphql_from_testdata(testdata, shard) + run_queries(gql_wrapper) + end, + servers = {'shard1', 'shard2', 'shard3', 'shard4'}, + use_tcp = false, + }) +end + return utils