diff --git a/db/schema.sql b/db/schema.sql index f55facf6..9af33148 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -12,24 +12,6 @@ create table if not exists principals ( create index "principals.idx-segment" on principals using hash (segment); -create table if not exists records ( - space_id text not null, - principal_id text not null, - resource_type text not null, - resource_id text not null, - attrs jsonb, - _rev integer not null, - - constraint "records.pkey" primary key (space_id, principal_id, resource_type, resource_id), - constraint "records.fkey-space_id+principal_id" foreign key (space_id, principal_id) - references principals(space_id, id) - on delete cascade, - - constraint "records.check-resource_type" check (resource_type <> ''), - constraint "records.check-resource_id" check (resource_id <> ''), - constraint "records.check-attrs" check (jsonb_typeof(attrs) = 'object') -); - create table if not exists tuples ( space_id text not null, -- used for creating data silos to support multi-tenancy strand text not null, -- a relation that connects two tuples together diff --git a/src/db/CMakeLists.txt b/src/db/CMakeLists.txt index 8dc6b52b..0758532f 100644 --- a/src/db/CMakeLists.txt +++ b/src/db/CMakeLists.txt @@ -3,7 +3,6 @@ target_sources(db PRIVATE pg.cpp principals.cpp - records.cpp tuples.cpp PUBLIC FILE_SET headers TYPE HEADERS @@ -11,7 +10,6 @@ target_sources(db config.h pg.h principals.h - records.h tuples.h PRIVATE FILE_SET private_headers TYPE HEADERS @@ -52,7 +50,6 @@ if (SENTIUM_BUILD_TESTING) PRIVATE pg_test.cpp principals_test.cpp - records_test.cpp tuples_test.cpp ) diff --git a/src/db/records.cpp b/src/db/records.cpp deleted file mode 100644 index cda1df52..00000000 --- a/src/db/records.cpp +++ /dev/null @@ -1,203 +0,0 @@ -#include "records.h" - -#include - -#include "err/errors.h" - -namespace db { -Record::Record(const Record::Data &data) noexcept : _data(data), _rev(0) {} - -Record::Record(Record::Data &&data) noexcept : _data(std::move(data)), _rev(0) {} - -Record::Record(const pg::row_t &r) : - _data({ - .attrs = r["attrs"].as(), - .principalId = r["principal_id"].as(), - .resourceId = r["resource_id"].as(), - .resourceType = r["resource_type"].as(), - .spaceId = r["space_id"].as(), - }), - _rev(r["_rev"].as()) {} - -bool Record::discard( - std::string_view spaceId, const std::string &principalId, const std::string &resourceType, - const std::string &resourceId) { - std::string_view qry = R"( - delete from records - where - space_id = $1::text - and principal_id = $2::text - and resource_type = $3::text - and resource_id = $4::text - ; - )"; - - auto res = pg::exec(qry, spaceId, principalId, resourceType, resourceId); - return (res.affected_rows() == 1); -} - -std::optional Record::lookup( - std::string_view spaceId, const std::string &principalId, const std::string &resourceType, - const std::string &resourceId) { - std::string_view qry = R"( - select - space_id, - principal_id, - resource_type, - resource_id, - attrs, - _rev - from records - where - space_id = $1::text - and principal_id = $2::text - and resource_type = $3::text - and resource_id = $4::text - ; - )"; - - auto res = pg::exec(qry, spaceId, principalId, resourceType, resourceId); - if (res.empty()) { - return std::nullopt; - } - - return Record(res[0]); -} - -void Record::store() { - std::string_view qry = R"( - insert into records as t ( - space_id, - principal_id, - resource_type, - resource_id, - attrs, - _rev - ) values ( - $1::text, - $2::text, - $3::text, - $4::text, - $5::jsonb, - $6::integer - ) - on conflict (space_id, principal_id, resource_type, resource_id) - do update - set ( - attrs, - _rev - ) = ( - $5::jsonb, - excluded._rev + 1 - ) - where t._rev = $6::integer - returning _rev; - )"; - - pg::result_t res; - try { - res = pg::exec( - qry, - _data.spaceId, - _data.principalId, - _data.resourceType, - _data.resourceId, - _data.attrs, - _rev); - } catch (pqxx::check_violation &) { - throw err::DbRecordInvalidData(); - } catch (pg::fkey_violation_t &) { - throw err::DbRecordInvalidPrincipalId(); - } - - if (res.empty()) { - throw err::DbRevisionMismatch(); - } - - _rev = res.at(0, 0).as(); -} - -Records ListRecordsByPrincipal( - std::string_view spaceId, std::string_view principalId, std::string_view resourceType, - std::string_view lastId, std::uint16_t count) { - std::string where = - "where space_id = $1::text and principal_id = $2::text and resource_type = $3::text"; - if (!lastId.empty()) { - where += " and resource_id < $4::text"; - } - - const std::string qry = fmt::format( - R"( - select - space_id, - principal_id, - resource_type, - resource_id, - attrs, - _rev - from records - {} - order by resource_id desc - limit {:d} - )", - where, - count); - - db::pg::result_t res; - if (!lastId.empty()) { - res = pg::exec(qry, spaceId, principalId, resourceType, lastId); - } else { - res = pg::exec(qry, spaceId, principalId, resourceType); - } - - Records records; - records.reserve(res.affected_rows()); - for (const auto &r : res) { - records.emplace_back(r); - } - - return records; -} - -Records ListRecordsByResource( - std::string_view spaceId, std::string_view resourceType, std::string_view resourceId, - std::string_view lastId, std::uint16_t count) { - std::string where = - "where space_id = $1::text and resource_type = $2::text and resource_id = $3::text"; - if (!lastId.empty()) { - where += " and principal_id < $4::text"; - } - - const std::string qry = fmt::format( - R"( - select - space_id, - principal_id, - resource_type, - resource_id, - attrs, - _rev - from records - {} - order by principal_id desc - limit {:d} - )", - where, - count); - - db::pg::result_t res; - if (!lastId.empty()) { - res = pg::exec(qry, spaceId, resourceType, resourceId, lastId); - } else { - res = pg::exec(qry, spaceId, resourceType, resourceId); - } - - Records records; - records.reserve(res.affected_rows()); - for (const auto &r : res) { - records.emplace_back(r); - } - - return records; -} -} // namespace db diff --git a/src/db/records.h b/src/db/records.h deleted file mode 100644 index 0257efe0..00000000 --- a/src/db/records.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "pg.h" - -namespace db { -class Record { -public: - struct Data { - using attrs_t = std::optional; - - attrs_t attrs; - std::string principalId; - std::string resourceId; - std::string resourceType; - std::string spaceId; - - bool operator==(const Data &) const noexcept = default; - }; - - Record(const Data &data) noexcept; - Record(Data &&data) noexcept; - - Record(const pg::row_t &r); - - bool operator==(const Record &) const noexcept = default; - - const Data::attrs_t &attrs() const noexcept { return _data.attrs; } - void attrs(const Data::attrs_t &attrs) noexcept { _data.attrs = attrs; } - void attrs(const std::string &attrs) noexcept { _data.attrs = attrs; } - void attrs(Data::attrs_t &&attrs) noexcept { _data.attrs = std::move(attrs); } - void attrs(std::string &&attrs) noexcept { _data.attrs = std::move(attrs); } - - const std::string &principalId() const noexcept { return _data.principalId; } - const std::string &resourceId() const noexcept { return _data.resourceId; } - const std::string &resourceType() const noexcept { return _data.resourceType; } - const std::string &spaceId() const noexcept { return _data.spaceId; } - - const int &rev() const noexcept { return _rev; } - - void store(); - - static bool discard( - std::string_view spaceId, const std::string &principalId, const std::string &resourceType, - const std::string &resourceId); - - static std::optional lookup( - std::string_view spaceId, const std::string &principalId, const std::string &resourceType, - const std::string &resourceId); - -private: - Data _data; - int _rev; -}; - -using Records = std::vector; - -Records ListRecordsByPrincipal( - std::string_view spaceId, std::string_view principalId, std::string_view resourceType, - std::string_view lastId = "", std::uint16_t count = 10); - -Records ListRecordsByResource( - std::string_view spaceId, std::string_view resourceType, std::string_view resourceId, - std::string_view lastId = "", std::uint16_t count = 10); -} // namespace db diff --git a/src/db/records_test.cpp b/src/db/records_test.cpp deleted file mode 100644 index 20096d79..00000000 --- a/src/db/records_test.cpp +++ /dev/null @@ -1,387 +0,0 @@ -#include - -#include "err/errors.h" - -#include "principals.h" -#include "records.h" -#include "testing.h" - -class db_RecordsTest : public ::testing::Test { -protected: - static void SetUpTestSuite() { - db::testing::setup(); - - // Clear data - db::pg::exec("truncate table principals cascade;"); - db::pg::exec("truncate table records;"); - } - - void SetUp() { - // Clear data before each test - db::pg::exec("delete from records;"); - } - - static void TearDownTestSuite() { db::testing::teardown(); } -}; - -TEST_F(db_RecordsTest, discard) { - db::Principal principal({ - .id = "id:db_RecordsTest.discard", - }); - ASSERT_NO_THROW(principal.store()); - - db::Record record({ - .principalId = principal.id(), - .resourceId = "discard", - .resourceType = "db_RecordsTest", - .spaceId = principal.spaceId(), - }); - ASSERT_NO_THROW(record.store()); - - bool result = false; - ASSERT_NO_THROW( - result = db::Record::discard( - record.spaceId(), record.principalId(), record.resourceType(), record.resourceId())); - EXPECT_TRUE(result); - - std::string_view qry = R"( - select - count(*) - from records - where - space_id = $1::text - and principal_id = $2::text - and resource_type = $3::text - and resource_id = $4::text - ; - )"; - - auto res = db::pg::exec( - qry, record.spaceId(), record.principalId(), record.resourceType(), record.resourceId()); - ASSERT_EQ(1, res.size()); - - auto count = res.at(0, 0).as(); - EXPECT_EQ(0, count); -} - -TEST_F(db_RecordsTest, list) { - db::Principal principal({ - .id = "id:db_RecordsTest.list", - }); - ASSERT_NO_THROW(principal.store()); - - db::Record record({ - .principalId = principal.id(), - .resourceId = "list", - .resourceType = "db_RecordsTest", - .spaceId = principal.spaceId(), - }); - ASSERT_NO_THROW(record.store()); - - // Success: list by principal - { - db::Records results; - ASSERT_NO_THROW( - results = db::ListRecordsByPrincipal( - principal.spaceId(), principal.id(), record.resourceType())); - ASSERT_EQ(1, results.size()); - - EXPECT_EQ(record, results[0]); - } - - // Success: list by resource - { - db::Records results; - ASSERT_NO_THROW( - results = db::ListRecordsByResource( - record.spaceId(), record.resourceType(), record.resourceId())); - ASSERT_EQ(1, results.size()); - - EXPECT_EQ(record, results[0]); - } - - // Success: list with last id - { - db::Principals principals({ - {{.id = "id:db_RecordsTest.list-with_last_id[0]"}}, - {{.id = "id:db_RecordsTest.list-with_last_id[1]"}}, - }); - - for (auto &p : principals) { - ASSERT_NO_THROW(p.store()); - } - - db::Records records({ - {{ - .principalId = principals[0].id(), - .resourceId = "list-with_last_id[0]", - .resourceType = "db_RecordsTest", - .spaceId = principals[0].spaceId(), - }}, - {{ - .principalId = principals[1].id(), - .resourceId = "list-with_last_id[0]", - .resourceType = "db_RecordsTest", - .spaceId = principals[1].spaceId(), - }}, - {{ - .principalId = principals[0].id(), - .resourceId = "list-with_last_id[1]", - .resourceType = "db_RecordsTest", - .spaceId = principals[0].spaceId(), - }}, - {{ - .principalId = principals[1].id(), - .resourceId = "list-with_last_id[1]", - .resourceType = "db_RecordsTest", - .spaceId = principals[1].spaceId(), - }}, - }); - - for (auto &r : records) { - ASSERT_NO_THROW(r.store()); - } - - // by principal - { - db::Records results; - ASSERT_NO_THROW({ - results = db::ListRecordsByPrincipal( - principals[0].spaceId(), - principals[0].id(), - records[0].resourceType(), - records[2].resourceId()); - }); - - ASSERT_EQ(1, results.size()); - EXPECT_EQ(records[0], results[0]); - } - - // by resource - { - db::Records results; - ASSERT_NO_THROW({ - results = db::ListRecordsByResource( - records[2].spaceId(), - records[2].resourceType(), - records[3].resourceId(), - principals[1].id()); - }); - - ASSERT_EQ(1, results.size()); - EXPECT_EQ(records[2], results[0]); - } - } -} - -TEST_F(db_RecordsTest, lookup) { - db::Principal principal({ - .id = "id:db_RecordsTest.lookup", - }); - ASSERT_NO_THROW(principal.store()); - - db::Record record({ - .principalId = principal.id(), - .resourceId = "lookup", - .resourceType = "db_RecordsTest", - .spaceId = principal.spaceId(), - }); - ASSERT_NO_THROW(record.store()); - - // Success: lookup record - { - auto result = db::Record::lookup( - record.spaceId(), record.principalId(), record.resourceType(), record.resourceId()); - ASSERT_TRUE(result); - - EXPECT_EQ(record, result.value()); - } - - // Success: lookup non-existent record - { - auto result = db::Record::lookup( - record.spaceId(), record.principalId(), record.resourceType(), "non-existent"); - EXPECT_EQ(std::nullopt, result); - } -} - -TEST_F(db_RecordsTest, rev) { - db::Principal principal({ - .id = "id:db_RecordsTest.rev", - }); - ASSERT_NO_THROW(principal.store()); - - // Success: revision increment - { - db::Record record({ - .principalId = principal.id(), - .resourceId = "rev", - .resourceType = "db_RecordsTest", - .spaceId = principal.spaceId(), - }); - - ASSERT_NO_THROW(record.store()); - EXPECT_EQ(0, record.rev()); - - ASSERT_NO_THROW(record.store()); - EXPECT_EQ(1, record.rev()); - } - - // Error: revision mismatch - { - db::Record record({ - .principalId = principal.id(), - .resourceId = "rev-mismatch", - .resourceType = "db_RecordsTest", - .spaceId = principal.spaceId(), - }); - - std::string_view qry = R"( - insert into records ( - space_id, - principal_id, - resource_type, - resource_id, - _rev - ) values ( - $1::text, - $2::text, - $3::text, - $4::text, - $5::integer - ) - )"; - - ASSERT_NO_THROW(db::pg::exec( - qry, - record.spaceId(), - record.principalId(), - record.resourceType(), - record.resourceId(), - record.rev() + 1)); - - EXPECT_THROW(record.store(), err::DbRevisionMismatch); - } -} - -TEST_F(db_RecordsTest, store) { - db::Principal principal({ - .id = "id:db_RecordsTest.store", - }); - ASSERT_NO_THROW(principal.store()); - - // Success: persist data - { - db::Record record({ - .principalId = principal.id(), - .resourceId = "store", - .resourceType = "db_RecordsTest", - .spaceId = principal.spaceId(), - }); - ASSERT_NO_THROW(record.store()); - - std::string_view qry = R"( - select - attrs, - _rev - from records - where - space_id = $1::text - and principal_id = $2::text - and resource_type = $3::text - and resource_id = $4::text - ; - )"; - - auto res = db::pg::exec( - qry, - record.spaceId(), - record.principalId(), - record.resourceType(), - record.resourceId()); - ASSERT_EQ(1, res.size()); - - auto [attrs, _rev] = res[0].as(); - EXPECT_EQ(record.rev(), _rev); - EXPECT_FALSE(attrs); - } - - // Success:: persist data with optional attrs - { - db::Record record({ - .attrs = R"( - { - "flag": true, - "name": "First Last", - "tags": [ - "test" - ] - } - )", - .principalId = principal.id(), - .resourceId = "store", - .resourceType = "db_RecordsTest", - .spaceId = principal.spaceId(), - }); - ASSERT_NO_THROW(record.store()); - - std::string_view qry = R"( - select - attrs->'flag' as flag, - attrs->>'name' as name, - attrs->'tags' as tags - from records - where - principal_id = $1::text - and resource_type = $2::text - and resource_id = $3::text - ; - )"; - - auto res = - db::pg::exec(qry, record.principalId(), record.resourceType(), record.resourceId()); - ASSERT_EQ(1, res.size()); - - auto [flag, name, tags] = res[0].as(); - EXPECT_EQ(true, flag); - EXPECT_EQ("First Last", name); - EXPECT_EQ(R"(["test"])", tags); - } - - // Error: invalid `spaceId` - { - db::Record record({ - .principalId = principal.id(), - .resourceId = "store-invalid_space_id", - .resourceType = "db_RecordsTest", - .spaceId = "space_id:db_RecordsTest.store-invalid_space_id", - }); - - EXPECT_THROW(record.store(), err::DbRecordInvalidPrincipalId); - } - - // Error: invalid `principalId` - { - db::Record record({ - .principalId = "id:db_RecordsTest.store-invalid_principal_id", - .resourceId = "store-invalid_principal_id", - .resourceType = "db_RecordsTest", - .spaceId = principal.spaceId(), - }); - - EXPECT_THROW(record.store(), err::DbRecordInvalidPrincipalId); - } - - // Error: invalid `attrs` - { - db::Record record({ - .attrs = R"("string")", - .principalId = principal.id(), - .resourceId = "store-invalid_attrs", - .resourceType = "db_RecordsTest", - .spaceId = principal.spaceId(), - }); - - EXPECT_THROW(record.store(), err::DbRecordInvalidData); - } -}