kong/spec/01-unit/01-db/04-dao_spec.lua (542 lines of code) (raw):

local Schema = require("kong.db.schema.init") local Entity = require("kong.db.schema.entity") local DAO = require("kong.db.dao.init") local errors = require("kong.db.errors") local utils = require("kong.tools.utils") local hooks = require("kong.hooks") local null = ngx.null local nullable_schema_definition = { name = "Foo", primary_key = { "a" }, fields = { { a = { type = "number" }, }, { b = { type = "string", default = "hello" }, }, { u = { type = "string" }, }, { r = { type = "record", required = false, fields = { { f1 = { type = "number" } }, { f2 = { type = "string", default = "world" } }, } } }, } } local non_nullable_schema_definition = { name = "Foo", primary_key = { "a" }, fields = { { a = { type = "number" }, }, { b = { type = "string", default = "hello", required = true }, }, { u = { type = "string" }, }, { r = { type = "record", fields = { { f1 = { type = "number" } }, { f2 = { type = "string", default = "world", required = true } }, } } }, } } local ttl_schema_definition = { name = "Foo", ttl = true, primary_key = { "a" }, fields = { { a = { type = "number" }, }, } } local optional_cache_key_fields_schema = { name = "Foo", primary_key = { "a" }, cache_key = { "b", "u" }, fields = { { a = { type = "number" }, }, { b = { type = "string" }, }, { u = { type = "string" }, }, }, } local parent_cascade_delete_schema = { name = "Foo", primary_key = { "a" }, fields = { { a = { type = "number" }, }, }, } local cascade_delete_schema = { name = "Bar", primary_key = { "b" }, fields = { { b = { type = "number" }, }, { c = { type = "foreign", reference = "Foo", on_delete = "cascade" }, }, }, } local mock_db = {} describe("DAO", function() describe("select", function() it("applies defaults if strategy returns column as nil and is nullable in schema", function() local schema = assert(Schema.new(nullable_schema_definition)) -- mock strategy local strategy = { select = function() return { a = 42, b = nil, r = { f1 = 10 } } end, } local dao = DAO.new(mock_db, schema, strategy, errors) local row = dao:select({ a = 42 }) assert.same(42, row.a) assert.same("hello", row.b) assert.same(10, row.r.f1) assert.same("world", row.r.f2) end) it("applies defaults if strategy returns column as nil and is not nullable in schema", function() local schema = assert(Schema.new(non_nullable_schema_definition)) -- mock strategy local strategy = { select = function() return { a = 42, b = nil, r = { f1 = 10 } } end, } local dao = DAO.new(mock_db, schema, strategy, errors) local row = dao:select({ a = 42 }) assert.same(42, row.a) assert.same("hello", row.b) assert.same(10, row.r.f1) assert.same("world", row.r.f2) end) it("applies defaults if strategy returns column as null and is not nullable in schema", function() local schema = assert(Schema.new(non_nullable_schema_definition)) -- mock strategy local strategy = { select = function() return { a = 42, b = null, r = { f1 = 10, f2 = null } } end, } local dao = DAO.new(mock_db, schema, strategy, errors) local row = dao:select({ a = 42 }) assert.same(42, row.a) assert.same("hello", row.b) assert.same(10, row.r.f1) assert.same("world", row.r.f2) end) it("preserves null if strategy returns column as null and is nullable in schema", function() local schema = assert(Schema.new(nullable_schema_definition)) -- mock strategy local strategy = { select = function() return { a = 42, b = null, r = { f1 = 10, f2 = null } } end, } local dao = DAO.new(mock_db, schema, strategy, errors) local row = dao:select({ a = 42 }, { nulls = true }) assert.same(42, row.a) assert.same(null, row.b) assert.same(10, row.r.f1) assert.same(null, row.r.f2) end) it("only returns a null ttl if nulls is given (#5185)", function() local schema = assert(Schema.new(ttl_schema_definition)) -- mock strategy local strategy = { select = function() return { a = 42, ttl = null } end, } local dao = DAO.new(mock_db, schema, strategy, errors) local row = dao:select({ a = 42 }, { nulls = true }) assert.same(42, row.a) assert.same(null, row.ttl) row = dao:select({ a = 42 }, { nulls = false }) assert.same(42, row.a) assert.same(nil, row.ttl) end) end) describe("update", function() it("does pre-apply defaults on partial update if field is nullable in schema", function() local schema = assert(Schema.new(nullable_schema_definition)) -- mock strategy local data local strategy = { select = function() return data end, update = function(_, _, value) -- defaults pre-applied before partial update assert(value.b == "hello") data = utils.deep_merge(data, value) return data end, } local dao = DAO.new(mock_db, schema, strategy, errors) data = { a = 42, b = nil, u = nil, r = nil } local row, err = dao:update({ a = 42 }, { u = "foo" }) assert.falsy(err) assert.same({ a = 42, b = "hello", u = "foo" }, row) end) it("does not pre-apply defaults on record fields if field is nullable in schema", function() local schema = assert(Schema.new(nullable_schema_definition)) -- mock strategy local data local strategy = { select = function() return data end, update = function(_, _, value) -- no defaults pre-applied before partial update assert(value.r.f2 == nil) data = utils.deep_merge(data, value) return data end, } local dao = DAO.new(mock_db, schema, strategy, errors) data = { a = 42, b = nil, u = nil, r = nil } local row, err = dao:update({ a = 42 }, { u = "foo", r = { f1 = 10 } }) assert.falsy(err) assert.same({ a = 42, b = "hello", u = "foo", r = { f1 = 10, f2 = "world" } }, row) end) it("always returns the structure of records when using Entities", function() local entity = assert(Entity.new(non_nullable_schema_definition)) -- mock strategy local data local strategy = { select = function() return data end, update = function(_, _, value) -- defaults pre-applied before partial update assert.equal("hello", value.b) assert.same({ f1 = null, f2 = "world", }, value.r) data = utils.deep_merge(data, value) return data end, } local dao = DAO.new(mock_db, entity, strategy, errors) data = { a = 42, b = nil, u = nil, r = nil } local row, err = dao:update({ a = 42 }, { u = "foo" }, { nulls = true }) assert.falsy(err) assert.same({ a = 42, b = "hello", u = "foo", r = { f1 = ngx.null, f2 = "world" } }, row) end) it("does apply defaults on entity if record is nullable in schema", function() local schema = assert(Schema.new(non_nullable_schema_definition)) -- mock strategy local data local strategy = { select = function() return data end, update = function(_, _, value) -- defaults pre-applied before partial update assert.equal("hello", value.b) assert.same(null, value.r) data = utils.deep_merge(data, value) return data end, } local dao = DAO.new(mock_db, schema, strategy, errors) data = { a = 42, b = nil, u = nil, r = nil } local row, err = dao:update({ a = 42 }, { u = "foo" }, { nulls = true }) assert.falsy(err) -- defaults are applied when returning the full updated entity assert.same({ a = 42, b = "hello", u = "foo", r = null }, row) end) it("applies defaults on entity for record in Entity", function() local schema = assert(Entity.new(non_nullable_schema_definition)) -- mock strategy local data local strategy = { select = function() return data end, update = function(_, _, value) data = utils.deep_merge(data, value) return data end, } local dao = DAO.new(mock_db, schema, strategy, errors) data = { a = 42, b = nil, u = nil, r = nil } local row, err = dao:update({ a = 42 }, { u = "foo" }, { nulls = true }) assert.falsy(err) -- defaults are applied when returning the full updated entity assert.same({ a = 42, b = "hello", u = "foo", r = { f1 = null, f2 = "world" } }, row) -- likewise for record update: data = { a = 42, b = nil, u = nil, r = nil } row, err = dao:update({ a = 43 }, { u = "foo", r = { f1 = 10 } }) assert.falsy(err) assert.same({ a = 42, b = "hello", u = "foo", r = { f1 = 10, f2 = "world" } }, row) end) it("applies defaults if strategy returns column as null and is not nullable in schema", function() local schema = assert(Schema.new(non_nullable_schema_definition)) -- mock strategy local strategy = { select = function() return {} end, update = function() return { a = 42, b = null, r = { f1 = 10, f2 = null } } end, } local dao = DAO.new(mock_db, schema, strategy, errors) local row = dao:update({ a = 42 }, { u = "foo" }) assert.same(42, row.a) assert.same("hello", row.b) assert.same(10, row.r.f1) assert.same("world", row.r.f2) end) it("preserves null if strategy returns column as null and is nullable in schema", function() local schema = assert(Schema.new(nullable_schema_definition)) -- mock strategy local data local strategy = { select = function() return data end, update = function(_, _, value) data = utils.deep_merge(data, value) return data end, } local dao = DAO.new(mock_db, schema, strategy, errors) data = { a = 42, b = null, u = null, r = null } local row, err = dao:update({ a = 42 }, { u = "foo" }, { nulls = true }) assert.falsy(err) assert.same({ a = 42, b = null, u = "foo", r = null }, row) end) it("sets default in r.f2 when setting r.f1 and r is currently nil", function() local schema = assert(Schema.new(nullable_schema_definition)) -- mock strategy local data local strategy = { select = function() return data end, update = function(_, _, value) data = utils.deep_merge(data, value) return data end, } local dao = DAO.new(mock_db, schema, strategy, errors) data = { a = 42, b = null, u = null, r = nil } local row, err = dao:update({ a = 43 }, { u = "foo", r = { f1 = 10 } }, { nulls = true }) assert.falsy(err) assert.same({ a = 42, b = null, u = "foo", r = { f1 = 10, f2 = "world" } }, row) end) it("sets default in r.f2 when setting r.f1 and r is currently nil", function() local schema = assert(Schema.new(non_nullable_schema_definition)) -- mock strategy local data local strategy = { select = function() return data end, update = function(_, _, value) data = utils.deep_merge(data, value) return data end, } local dao = DAO.new(mock_db, schema, strategy, errors) data = { a = 42, b = nil, u = null, r = nil } local row, err = dao:update({ a = 43 }, { u = "foo", r = { f1 = 10 } }, { nulls = true }) assert.falsy(err) assert.same({ a = 42, b = "hello", u = "foo", r = { f1 = 10, f2 = "world" } }, row) end) it("sets default in r.f2 when setting r.f1 and r is currently null", function() local schema = assert(Schema.new(nullable_schema_definition)) -- mock strategy local data local strategy = { select = function() return data end, update = function(_, _, value) data = utils.deep_merge(data, value) return data end, } local dao = DAO.new(mock_db, schema, strategy, errors) data = { a = 42, b = null, u = nil, r = nil } local row, err = dao:update({ a = 43 }, { u = "foo", r = { f1 = 10 } }, { nulls = true }) assert.falsy(err) assert.same({ a = 42, b = null, u = "foo", r = { f1 = 10, f2 = "world" } }, row) end) it("preserves null in r.f2 when setting r.f1", function() local schema = assert(Schema.new(nullable_schema_definition)) -- mock strategy local data local strategy = { select = function() return data end, update = function(_, _, value) data = utils.deep_merge(data, value) return data end, } local dao = DAO.new(mock_db, schema, strategy, errors) -- setting r.f2 as an explicit null data = { a = 42, b = null, u = null, r = { f1 = 9, f2 = null } } local row, err = dao:update({ a = 43 }, { u = "foo", r = { f1 = 10, f2 = null } }, { nulls = true }) assert.falsy(err) assert.same({ a = 42, b = null, u = "foo", r = { f1 = 10, f2 = null } }, row) end) end) describe("delete", function() lazy_setup(function() local kong_global = require "kong.global" _G.kong = kong_global.new() end) it("deletes the entity and cascades the delete notifications", function() local parent_schema = assert(Schema.new(parent_cascade_delete_schema)) local child_schema = assert(Schema.new(cascade_delete_schema)) -- mock strategy local data = { a = 42, b = nil, u = nil, r = nil } local child_strategy = { each_for_c = function() return {}, nil end, page_for_c = function() return {}, nil end } local child_dao = DAO.new(mock_db, child_schema, child_strategy, errors) mock_db = { daos = { Bar = child_dao } } local parent_strategy = { select = function() return data end, delete = function(pk, _) -- assert.are.same({ a = 42 }, pk) return nil, nil end } local parent_dao = DAO.new(mock_db, parent_schema, parent_strategy, errors) local _, err = parent_dao:delete({ a = 42 }) assert.falsy(err) end) it("find_cascade_delete_entities()", function() local parent_schema = assert(Schema.new({ name = "Foo", primary_key = { "a" }, fields = { { a = { type = "number" }, }, } })) local child_schema = assert(Schema.new({ name = "Bar", primary_key = { "b" }, fields = { { b = { type = "number" }, }, { c = { type = "foreign", reference = "Foo", on_delete = "cascade" }, }, } })) local parent_strategy = setmetatable({}, {__index = function() return function() end end}) local child_strategy = parent_strategy local child_dao = DAO.new(mock_db, child_schema, child_strategy, errors) child_dao.each_for_c = function() local i = 0 return function() i = i + 1 if i == 1 then return { c = 40 } end end end -- Create grandchild schema local grandchild_schema = assert(Schema.new({ name = "Dar", primary_key = { "d" }, fields = { { d = { type = "number" }, }, { e = { type = "foreign", reference = "Bar", on_delete = "cascade" }, }, } })) local parent_strategy = setmetatable({}, {__index = function() return function() end end}) local grandchild_strategy = parent_strategy local grandchild_dao = DAO.new(mock_db, grandchild_schema, grandchild_strategy, errors) grandchild_dao.each_for_e = function() local i = 0 return function() i = i + 1 -- We have 3 grand child entities if i <= 3 then return { e = 50 + i } end end end -- Create great_grandchild schema local great_grandchild_schema = assert(Schema.new({ name = "Far", primary_key = { "f" }, fields = { { f = { type = "number" }, }, { g = { type = "foreign", reference = "Dar", on_delete = "cascade" }, }, } })) local parent_strategy = setmetatable({}, {__index = function() return function() end end}) local great_grandchild_strategy = parent_strategy local great_grandchild_dao = DAO.new(mock_db, great_grandchild_schema, great_grandchild_strategy, errors) great_grandchild_dao.each_for_g = function() local i = 0 return function() i = i + 1 -- We have 3 great grand child entities if i <= 3 then return { g = 60 + i } end end end mock_db = { daos = { Bar = child_dao, Dar = grandchild_dao, Far = great_grandchild_dao, } } local parent_dao = DAO.new(mock_db, parent_schema, parent_strategy, errors) local parent_entity = {} local entries = DAO._find_cascade_delete_entities(parent_dao, parent_entity, { show_ws_id = true }) assert.equal(#entries, 13) -- Entry 1 should be the child `c` entity which references the parent `Foo` DAO assert.equal(40, entries[1].entity.c) -- Entry 2 should be the grandchild `e` entity which references the child `Bar` DAO assert.equal(51, entries[2].entity.e) -- Entries 3 to 5 should be the great grandchild `g` entity which references the grandchild `Dar` DAO assert.equal(61, entries[3].entity.g) assert.equal(62, entries[4].entity.g) assert.equal(63, entries[5].entity.g) -- Entry 6 should be the grandchild `e` entity which references the child `Bar` DAO assert.equal(52, entries[6].entity.e) -- Entries 7 to 9 should be the great grandchild `g` entity which references the grandchild `Dar` DAO assert.equal(61, entries[7].entity.g) assert.equal(62, entries[8].entity.g) assert.equal(63, entries[9].entity.g) -- Entry 10 should be the grandchild `e` entity which references the child `Bar` DAO assert.equal(53, entries[10].entity.e) -- Entries 11 to 13 should be the great grandchild `g` entity which references the grandchild `Dar` DAO assert.equal(61, entries[11].entity.g) assert.equal(62, entries[12].entity.g) assert.equal(63, entries[13].entity.g) end) it("should call post-delete hook once after concurrent delete", function() finally(function() hooks.clear_hooks() end) local post_hook = spy.new(function() end) local delete_called = false hooks.register_hook("dao:delete:post", function() post_hook() end) local schema = Schema.new({ name = "Baz", primary_key = { "id" }, fields = { { id = { type = "number" } }, } }) local strategy = { select = function() return { id = 1 } end, delete = function(pk, _) if not delete_called then delete_called = true return true end return nil end } local dao = DAO.new({}, schema, strategy, errors) dao:delete({ id = 1 }) dao:delete({ id = 1 }) assert.spy(post_hook).was_called(1) end) end) describe("cache_key", function() it("converts null in composite cache_key to empty string", function() local schema = assert(Schema.new(optional_cache_key_fields_schema)) local dao = DAO.new(mock_db, schema, {}, errors) -- setting u as an explicit null local data = { a = 42, b = "foo", u = null } local cache_key = dao:cache_key(data) assert.equals("Foo:foo:::::", cache_key) end) it("converts nil in composite cache_key to empty string", function() local schema = assert(Schema.new(optional_cache_key_fields_schema)) local dao = DAO.new(mock_db, schema, {}, errors) local data = { a = 42, b = "foo", u = nil } local cache_key = dao:cache_key(data) assert.equals("Foo:foo:::::", cache_key) end) it("fallbacks to primary_key if nothing in cache_key is found", function() local schema = assert(Schema.new(optional_cache_key_fields_schema)) local dao = DAO.new(mock_db, schema, {}, errors) local data = { a = 42 } local cache_key = dao:cache_key(data) assert.equals("Foo:42:::::", cache_key) end) end) end)