From: jh9216.park Date: Mon, 5 Dec 2022 05:44:06 +0000 (-0500) Subject: Add sub package 'tizen-database' X-Git-Tag: accepted/tizen/unified/20221209.124050~5 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=69d9abb67720aba2c840da859556503954ddbd3a;p=platform%2Fcore%2Fbase%2Fbundle.git Add sub package 'tizen-database' - This package provides C++ wrapper API for sqlite3 Change-Id: I05fce9e6deb4922a9dd7aa45f6e3771f80f6cb1b Signed-off-by: jh9216.park --- diff --git a/CMakeLists.txt b/CMakeLists.txt index b355e28..f58604b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,8 @@ INSTALL(FILES ${CMAKE_BINARY_DIR}/bundle.pc DESTINATION ${LIB_INSTALL_DIR}/pkgco ADD_SUBDIRECTORY(parcel) ADD_DEPENDENCIES(parcel bundle) +ADD_SUBDIRECTORY(tizen-database) + IF(NOT DEFINED MINIMUM_BUILD) ENABLE_TESTING() SET(BUNDLE_UNITTESTS bundle_unittests) @@ -63,4 +65,8 @@ ADD_TEST(NAME ${PARCEL_UNITTESTS} COMMAND ${PARCEL_UNITTESTS} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests/parcel_unittests) ADD_DEPENDENCIES(parcel_unittests parcel) + +SET(TIZEN_DATABASE_UNITTESTS tizen-database_unittests) +ADD_TEST(NAME ${TIZEN_DATABASE_UNITTESTS} COMMAND ${TIZEN_DATABASE_UNITTESTS} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests/tizen-database_unittests) ENDIF(NOT DEFINED MINIMUM_BUILD) diff --git a/packaging/bundle.spec b/packaging/bundle.spec index 002de6c..b796cdf 100644 --- a/packaging/bundle.spec +++ b/packaging/bundle.spec @@ -12,6 +12,7 @@ BuildRequires: pkgconfig(glib-2.0) BuildRequires: pkgconfig(dlog) BuildRequires: pkgconfig(capi-base-common) BuildRequires: pkgconfig(json-glib-1.0) +BuildRequires: pkgconfig(sqlite3) BuildRequires: pkgconfig(gmock) %if 0%{?gcov:1} @@ -80,6 +81,26 @@ Group: Development/Libraries %description -n parcel-unittests GTest for parcel +################################################# +## tizen-database-devel +################################################# +%package -n tizen-database-devel +Summary: Tizen-database Library (devel) +Group: Development/Libraries + +%description -n tizen-database-devel +Tizen-database Library (devel) + +################################################# +# tizen-database-unittests +################################################# +%package -n tizen-database-unittests +Summary: GTest for tizen-database +Group: Development/Libraries + +%description -n tizen-database-unittests +GTest for tizen-database + %prep %setup -q -n %{name}-%{version} cp %{SOURCE1001} . @@ -195,6 +216,9 @@ EOF mkdir -p %{buildroot}%{_bindir}/tizen-unittests/parcel install -m 0755 run-parcel-unittest.sh %{buildroot}%{_bindir}/tizen-unittests/parcel/run-unittest.sh +mkdir -p %{buildroot}%{_bindir}/tizen-unittests/tizen-database +install -m 0755 run-parcel-unittest.sh %{buildroot}%{_bindir}/tizen-unittests/tizen-database/run-unittest.sh + %post -p /sbin/ldconfig %postun -p /sbin/ldconfig @@ -255,3 +279,14 @@ install -m 0755 run-parcel-unittest.sh %{buildroot}%{_bindir}/tizen-unittests/pa %files -n parcel-unittests %{_bindir}/parcel_unittests %{_bindir}/tizen-unittests/parcel/run-unittest.sh + +################################################# +# tizen-database-devel +################################################# +%files -n tizen-database-devel +%{_includedir}/tizen-database/* +%{_libdir}/pkgconfig/tizen-database.pc + +%files -n tizen-database-unittests +%{_bindir}/tizen-database_unittests +%{_bindir}/tizen-unittests/tizen-database/run-unittest.sh diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 30c8c6f..2091545 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,2 +1,3 @@ ADD_SUBDIRECTORY(bundle_unittests) ADD_SUBDIRECTORY(parcel_unittests) +ADD_SUBDIRECTORY(tizen-database_unittests) diff --git a/tests/tizen-database_unittests/CMakeLists.txt b/tests/tizen-database_unittests/CMakeLists.txt new file mode 100644 index 0000000..a7a36fd --- /dev/null +++ b/tests/tizen-database_unittests/CMakeLists.txt @@ -0,0 +1,29 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 2.8) +PROJECT(tizen-database_unittests CXX) + +INCLUDE(FindPkgConfig) +PKG_CHECK_MODULES(tizen-database_unittests REQUIRED + gmock + sqlite3 +) + +SET(EXTRA_CFLAGS "") +FOREACH(flag ${database_unittests_CFLAGS}) + SET(EXTRA_CFLAGS "${EXTRA_CFLAGS} ${flag}") +ENDFOREACH(flag) +SET(EXTRA_CFLAGS "${EXTRA_CFLAGS} -fvisibility=hidden -Wall -Werror") + +SET(CMAKE_CXX_FLAGS "${EXTRA_CFLAGS} -std=c++17") +SET(CMAKE_CXX_FLAGS_DEBUG "-O0 -g") +SET(CMAKE_CXX_FLAGS_RELEASE "-O2") + +INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/../../) +AUX_SOURCE_DIRECTORY(${CMAKE_CURRENT_SOURCE_DIR}/src UNITTESTS_SOURCES) + +ADD_EXECUTABLE(${PROJECT_NAME} + ${UNITTESTS_SOURCES} +) + +SET_TARGET_PROPERTIES(${PROJECT_NAME} PROPERTIES COMPILE_FLAGS "${EXTRA_CFLAGS}") +TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${tizen-database_unittests_LDFLAGS}) +INSTALL(TARGETS ${PROJECT_NAME} DESTINATION /usr/bin/) \ No newline at end of file diff --git a/tests/tizen-database_unittests/src/test_database.cc b/tests/tizen-database_unittests/src/test_database.cc new file mode 100644 index 0000000..52bff43 --- /dev/null +++ b/tests/tizen-database_unittests/src/test_database.cc @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include +#include +#include + +#include "tizen-database/database.hpp" + +namespace { + +constexpr const char TEST_DB[] = "test.db"; +constexpr const char Q_CREATE_TABLE[] = + "CREATE TABLE TestTable(name TEXT, num INTERGER, val REAL, data BLOB);"; +constexpr const char Q_DROP_TABLE[] = "DROP TABLE TestTable;"; +constexpr const char Q_INSERT[] = + "INSERT INTO TestTable(name, num, val, data) VALUES (?, ?, ?, ?);"; +constexpr const char Q_INSERT_POS[] = + "INSERT INTO TestTable(name, num, val, data) " + "VALUES (?101, ?102, ?103, ?104);"; +constexpr const char Q_INSERT_NAME[] = + "INSERT INTO TestTable(name, num, val, data) " + "VALUES (:name, :num, :val, :data);"; +constexpr char Q_SELECT[] = "SELECT * FROM TestTable;"; +constexpr char Q_UPDATE[] = "UPDATE TestTable SET name=?;"; +constexpr char Q_DELETE[] = "DELETE FROM TestTable WHERE name=?;"; + +} // namespace + +TEST(DBBasicTest, create_table) { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + auto r = db.Exec({ Q_CREATE_TABLE }); + + EXPECT_TRUE(static_cast(r)); +} + +TEST(DBBasicTest, drop_table) { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + db.Exec({ Q_CREATE_TABLE }); + auto r1 = db.Exec({ Q_DROP_TABLE }); + + EXPECT_TRUE(static_cast(r1)); +} + +TEST(DBBasicTest, db_move_ctor) { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + tizen_base::Database db2(std::move(db)); + + EXPECT_FALSE(static_cast(db)); + EXPECT_TRUE(static_cast(db2)); +} + +TEST(DBBasicTest, db_move_assignment) { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + tizen_base::Database db2; + db2 = std::move(db); + + EXPECT_FALSE(static_cast(db)); + EXPECT_TRUE(static_cast(db2)); +} + +TEST(DBBasicTest, busy_handler) { + constexpr useconds_t BUSY_WAITING_USEC = (1000000 / 10 / 2); /* 0.05 sec */ + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE | + SQLITE_OPEN_CREATE, [&](int count) { + usleep(BUSY_WAITING_USEC); + if (count < 10) + return true; + + return false; + }); + + EXPECT_TRUE(static_cast(db)); +} + +class ResultTest : public ::testing::Test { + public: + virtual void SetUp() { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + db.Exec({ Q_DROP_TABLE }); + } + + virtual void TearDown() { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + db.Exec({ Q_DROP_TABLE }); + } +}; + +TEST_F(ResultTest, result_move_ctor) { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + tizen_base::Database::Result r = db.Exec({ Q_CREATE_TABLE }); + ASSERT_TRUE(static_cast(r)); + tizen_base::Database::Result r2(std::move(r)); + + EXPECT_FALSE(static_cast(r)); + EXPECT_TRUE(static_cast(r2)); +} + +TEST_F(ResultTest, result_move_assignment) { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + tizen_base::Database::Result r = db.Exec({ Q_CREATE_TABLE }); + ASSERT_TRUE(static_cast(r)); + tizen_base::Database::Result r2; + ASSERT_FALSE(static_cast(r2)); + r2 = std::move(r); + + EXPECT_FALSE(static_cast(r)); + EXPECT_TRUE(static_cast(r2)); +} + +class DatabaseTest : public ::testing::Test { + public: + virtual void SetUp() { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE | + SQLITE_OPEN_CREATE); + db.Exec({ Q_CREATE_TABLE }); + } + + virtual void TearDown() { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + db.Exec({ Q_DROP_TABLE }); + } + + void SetDefault() { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto q = tizen_base::Database::Sql(Q_INSERT) + .Bind("gogo") + .Bind(1234) + .Bind(9.216) + .Bind(std::vector {'9', '2', '1', '6' }); + + auto r1 = db.Exec(q); + ASSERT_TRUE(static_cast(r1)); + auto r2 = db.Exec(q); + ASSERT_TRUE(static_cast(r2)); + } +}; + +TEST_F(DatabaseTest, test_insert) { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto q = tizen_base::Database::Sql(Q_INSERT) + .Bind("gogo") + .Bind(1234) + .Bind(9.216) + .Bind(std::vector {'9', '2', '1', '6' }); + auto r = db.Exec(q); + + EXPECT_TRUE(static_cast(r)); +} + +TEST_F(DatabaseTest, test_insert_pos) { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto q = tizen_base::Database::Sql(Q_INSERT_POS) + .Bind(101, "gogo") + .Bind(102, 1234) + .Bind(103, 9.216) + .Bind(104, std::vector {'9', '2', '1', '6' }); + auto r = db.Exec(q); + + EXPECT_TRUE(static_cast(r)); +} + +TEST_F(DatabaseTest, test_insert_name) { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto q = tizen_base::Database::Sql(Q_INSERT_NAME) + .Bind(":name", "gogo") + .Bind(":num", 1234) + .Bind(":val", 9.216) + .Bind(":data", std::vector {'9', '2', '1', '6' }); + auto r = db.Exec(q); + + EXPECT_TRUE(static_cast(r)); +} + +TEST_F(DatabaseTest, test_select) { + SetDefault(); + using tizen_base::_; + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto r = db.Exec({ Q_SELECT }); + + EXPECT_TRUE(static_cast(r)); + for (const auto& i : r) { + auto [name, num, val, data] = i.Get<_, _, _, _>(); + EXPECT_EQ(static_cast(name), "gogo"); + EXPECT_EQ(static_cast(num), 1234); + EXPECT_EQ(static_cast(val), 9.216); + std::vector v = data; + EXPECT_EQ(v.size(), 4); + EXPECT_EQ(v[0], '9'); + EXPECT_EQ(v[1], '2'); + EXPECT_EQ(v[2], '1'); + EXPECT_EQ(v[3], '6'); + } +} + +TEST_F(DatabaseTest, test_select2) { + SetDefault(); + using tizen_base::_; + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto r = db.Exec({ Q_SELECT }); + + EXPECT_TRUE(static_cast(r)); + for (const auto& i : r) { + std::string name = i.Get(0); + double val = i.Get(2); + EXPECT_EQ(name, "gogo"); + EXPECT_EQ(val, 9.216); + } +} + +TEST_F(DatabaseTest, test_ToVector) { + SetDefault(); + + struct Rec { + std::string Name; + int Num; + double Val; + std::vector Data; + + void operator () (const tizen_base::Database::Result::Record& rec) { + Name = static_cast(rec.Get(0)); + Num = rec.Get(1); + Val = rec.Get(2); + Data = rec.Get(3); + } + }; + + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto table = db.Exec({ Q_SELECT }).ToVector(); + + EXPECT_GT(table.size(), 0); + for (const auto& i : table) { + EXPECT_EQ(i.Name, "gogo"); + EXPECT_EQ(i.Val, 9.216); + } +} + +TEST_F(DatabaseTest, test_ToList) { + SetDefault(); + + struct Rec { + std::string Name; + int Num; + double Val; + std::vector Data; + + void operator () (const tizen_base::Database::Result::Record& rec) { + Name = static_cast(rec.Get(0)); + Num = rec.Get(1); + Val = rec.Get(2); + Data = rec.Get(3); + } + }; + + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto table = db.Exec({ Q_SELECT }).ToList(); + + EXPECT_GT(table.size(), 0); + for (const auto& i : table) { + EXPECT_EQ(i.Name, "gogo"); + EXPECT_EQ(i.Val, 9.216); + } +} + +TEST_F(DatabaseTest, test_GetFirst) { + SetDefault(); + + struct Rec { + std::string Name; + int Num; + double Val; + std::vector Data; + + void operator () (const tizen_base::Database::Result::Record& rec) { + Name = static_cast(rec.Get(0)); + Num = rec.Get(1); + Val = rec.Get(2); + Data = rec.Get(3); + } + }; + + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto rec = db.Exec({ Q_SELECT }).GetFirst(); + + EXPECT_TRUE(rec); + EXPECT_EQ(rec->Name, "gogo"); + EXPECT_EQ(rec->Val, 9.216); +} + +TEST_F(DatabaseTest, test_GetFirstRecord) { + SetDefault(); + using tizen_base::_; + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto r = db.Exec({ Q_SELECT }); + + EXPECT_TRUE(static_cast(r)); + auto rec = r.GetFirstRecord(); + EXPECT_TRUE(rec); + + std::string name = rec->Get(0); + double val = rec->Get(2); + EXPECT_EQ(name, "gogo"); + EXPECT_EQ(val, 9.216); +} + +TEST_F(DatabaseTest, test_update) { + SetDefault(); + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto q = tizen_base::Database::Sql(Q_UPDATE) + .Bind("gugu"); + auto r = db.Exec(q); + + EXPECT_TRUE(static_cast(r)); +} + +TEST_F(DatabaseTest, test_delete) { + SetDefault(); + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto q = tizen_base::Database::Sql(Q_DELETE) + .Bind("gogo"); + auto r = db.Exec(q); + + EXPECT_TRUE(static_cast(r)); +} + +TEST_F(DatabaseTest, test_delete_n) { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READONLY); + auto q = tizen_base::Database::Sql(Q_DELETE) + .Bind("gogo"); + auto r = db.Exec(q); + + EXPECT_FALSE(static_cast(r)); + EXPECT_EQ(static_cast(r), SQLITE_READONLY); + EXPECT_STREQ(r, "attempt to write a readonly database"); +} + +TEST_F(DatabaseTest, test_transaction_commit) { + tizen_base::Database db(TEST_DB, SQLITE_OPEN_READWRITE); + auto guard = db.CreateTransactionGuard(); + auto q = tizen_base::Database::Sql(Q_INSERT) + .Bind("gogo") + .Bind(1234) + .Bind(9.216) + .Bind(std::vector {'9', '2', '1', '6' }); + auto r = db.Exec(q); + ASSERT_TRUE(static_cast(r)); + int ret = guard.Commit(); + + EXPECT_EQ(ret, SQLITE_OK); +} diff --git a/tests/tizen-database_unittests/src/test_main.cc b/tests/tizen-database_unittests/src/test_main.cc new file mode 100644 index 0000000..da13aab --- /dev/null +++ b/tests/tizen-database_unittests/src/test_main.cc @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include +#include + +#include + +int main(int argc, char** argv) { + try { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); + } catch (std::exception const &e) { + std::cout << "test_main caught exception: " << e.what() << std::endl; + return -1; + } +} diff --git a/tizen-database/CMakeLists.txt b/tizen-database/CMakeLists.txt new file mode 100644 index 0000000..9062594 --- /dev/null +++ b/tizen-database/CMakeLists.txt @@ -0,0 +1,9 @@ +### Make pkgconfig file +SET(PREFIX ${CMAKE_INSTALL_PREFIX}) +CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/tizen-database.pc.in + ${CMAKE_BINARY_DIR}/tizen-database.pc @ONLY) + +### Install +INSTALL(FILES ${CMAKE_BINARY_DIR}/tizen-database.pc DESTINATION ${LIB_INSTALL_DIR}/pkgconfig/) +INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/database.hpp + DESTINATION include/tizen-database) diff --git a/tizen-database/README.md b/tizen-database/README.md new file mode 100644 index 0000000..10c610a --- /dev/null +++ b/tizen-database/README.md @@ -0,0 +1,195 @@ +# TizenDatabase +Wrapper class for sqlite3 + +## Source +See database.hpp + +## Require +sqlite3, C++17 + +## How to use? +Add 'BuildRequires: pkgconfig(tizen-database)' in your .spec file. + +## Cookbook + +### Auto close +- It was implemented by RAII idiom. Database is automatically closed when it exits the scope. +```cpp +void test() { + tizen_base::Database db("test.db", SQLITE_OPEN_READWRITE); + ... +} +``` + +### Bind +- Bind() method supports various data type such as std::string, int, double and std::vector. + +```cpp + tizen_base::Database db("test.db", SQLITE_OPEN_READWRITE); + auto q = tizen_base::Database::Sql( + "INSERT INTO TestTable(name, num, val, data) VALUES (?, ?, ?, ?);") + .Bind("gogo") + .Bind(1234) + .Bind(9.216) + .Bind( std::vector {'9', '2', '1', '6' } ); +``` + +- Bind() method also supports named tag using ':'. +```cpp + tizen_base::Database db("test.db", SQLITE_OPEN_READWRITE); + auto q = tizen_base::Database::Sql("INSERT INTO TestTable(name, num, val, data) VALUES (:name, :num, :val, :data);") + .Bind(":name", "gogo") + .Bind(":num", 1234) + .Bind(":val", 9.216) + .Bind(":data", std::vector {'9', '2', '1', '6' }); +``` + +### Structured binding declaration +```cpp + auto [name, num, val, data] = i.Get<_, _, _, _>(); +``` +- Structured binding declaration can be used to unpack the record. + +```cpp + for (const auto& i : db.Exec({ "SELECT name, num, val, data FROM TestTable;" })) { + auto [name, num, val, data] = i.Get<_, _, _, _>(); + std::cout << static_cast(name) << std::endl; + } +``` + +### Range-based for loop +- class 'Result' is iterable. + +```cpp + tizen_base::Database::Result r = + db.Exec({ "SELECT name, num, val, data FROM TestTable;" }); + for (const auto& i : r) { + ... + } +``` + +### Explicit 'bool' type and 'int' type conversion operator +- 'bool' type operator is supported to check errors. +- 'int' type operator is supported to get error number. +```cpp + auto r = db.Exec(q); + if (!static_cast(r)) { + std::cout << "error code:" << static_cast(r) << std::endl; + } +``` + +### Implicit 'const char*' type conversion operator +- 'const char*' type operator is supported to get error string +```cpp + auto r = db.Exec(q); + if (!static_cast(r)) { + std::cout << "error message:" << r << std::endl; + } +``` + +### Extract values from query results +- There are many conversion operators to get the value. +```cpp + for (const auto& i : db.Exec({ "SELECT name, num, val, data FROM TestTable;" })) { + std::string name = i.Get(0); + int num = i.Get(1); + double val = i.Get(2); + std::vector data = i.Get(3); + } +``` + +### ToList(), ToVector() methods +- Once you provide function operator in your classes, you can call the methods. +- The class or struct should be movable. +```cpp + struct MyRec { + std::string Id; + int Val; + + // It should be implemented to map data set + void operator () (const tizen_base::Database::Result::Record& rec) { + Id = static_cast(rec.Get(0)); + Val = rec.Get(1); + } + }; + + auto /*std::vector*/ table = + db.Exec({ "SELECT id, val FROM TestTable;" }).ToVector(); +``` + +### Transaction guard +```cpp +void test() { + tizen_base::Database db("test.db", SQLITE_OPEN_READWRITE); + auto guard = db.CreateTransactionGuard(); + + auto r = db.Exec({ "INSERT INTO MyTable(name, id) VALUES('gogo', 1);" }); + if (!static_cast(r)) + return; // Rollback + + auto r2 = db.Exec({ "INSERT INTO MyTable(name, id) VALUES('gugu', 2);" }); + if (!static_cast(r2)) + return; // Rollback + + guard.Commit(); +} +``` + +### Busy handler +```cpp +void test() { + tizen_base::Database db("test.db", SQLITE_OPEN_READWRITE, [&](int count) { + // Busy handler + usleep(BUSY_WAITING_USEC); + if (count < 10) + return true; + + return false; + }); + ... +} +``` + +### Samples +```cpp +void test() { + tizen_base::Database db("test.db", SQLITE_OPEN_READWRITE); + for (const auto& i : db.Exec({ "SELECT name, num, val, data FROM TestTable;" })) { + auto [name, num, val, data] = i.Get<_, _, _, _>(); + std::cout << static_cast(name) << std::endl; + } +} +``` + +```cpp +void test() { + tizen_base::Database db("test.db", SQLITE_OPEN_READWRITE); + auto q = tizen_base::Database::Sql("INSERT INTO TestTable(name, num, val, data) VALUES (?, ?, ?, ?);") + .Bind("gogo") + .Bind(1234) + .Bind(9.216) + .Bind( std::vector {'9', '2', '1', '6' } ); + auto r = db.Exec(q); + if (!static_cast(r)) { + std::cout << "insert failed:" << r << std::endl; + std::cout << "error code:" << static_cast(r) << std::endl; + } +} +``` + +```cpp +void test() { + tizen_base::Database db("test.db", SQLITE_OPEN_READWRITE); + auto q = tizen_base::Database::Sql( + "INSERT INTO TestTable(name, num, val, data) VALUES (:name, :num, :val, :data);") + .Bind(":name", "gogo") + .Bind(":num", 1234) + .Bind(":val", 9.216) + .Bind(":data", std::vector {'9', '2', '1', '6' }); + auto r = db.Exec(q); + if (!static_cast(r)) { + std::cout << "insert failed:" << r << std::endl; + std::cout << "error code:" << static_cast(r) << std::endl; + } +} +``` diff --git a/tizen-database/database.hpp b/tizen-database/database.hpp new file mode 100644 index 0000000..9ace83c --- /dev/null +++ b/tizen-database/database.hpp @@ -0,0 +1,526 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef TIZEN_DATABASE_DATABASE_HPP_ +#define TIZEN_DATABASE_DATABASE_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tizen_base { + +template +struct num { + static const constexpr auto value = N; +}; + +template +void for_(F func, std::index_sequence) { + (func(num{}), ...); +} + +template +void for_(F func) { + for_(func, std::make_index_sequence()); +} + +using DbType = std::variant>; + +class AutoDbType { + public: + AutoDbType() = default; + AutoDbType(DbType db_type) : db_type_(db_type) {} + + operator int () { + return std::get(db_type_); + } + + operator std::string () { + return std::get(db_type_); + } + + operator double () { + return std::get(db_type_); + } + + operator std::vector () { + return std::get>(db_type_); + } + + private: + DbType db_type_; +}; + +using _ = AutoDbType; + +class Database { + public: + class TransactionGuard { + public: + TransactionGuard(const TransactionGuard&) = delete; + TransactionGuard& operator = (const TransactionGuard&) = delete; + + TransactionGuard(TransactionGuard&& t) noexcept { + db_ = t.db_; + t.db_ = nullptr; + } + + TransactionGuard& operator = (TransactionGuard&& t) noexcept { + if (this != &t) { + if (db_) + sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, nullptr); + db_ = t.db_; + t.db_ = nullptr; + } + + return *this; + } + + TransactionGuard(sqlite3* db) : db_(db) { + if (sqlite3_exec(db, "BEGIN DEFERRED", nullptr, nullptr, nullptr) + != SQLITE_OK) { + throw std::runtime_error("begin transaction failed"); + } + } + + ~TransactionGuard() { + if (db_) + sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, nullptr); + } + + int Commit() { + int ret = sqlite3_exec(db_, "COMMIT", nullptr, nullptr, nullptr); + if (ret != SQLITE_OK) { + sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, nullptr); + } + + db_ = nullptr; + return ret; + } + + private: + sqlite3* db_ = nullptr; + }; + + class Sql { + public: + Sql(std::string query) : query_(std::move(query)) {} + + Sql& Bind(std::string val) { + bindings_.push_back(DbType(std::move(val))); + return *this; + } + + Sql& Bind(int val) { + bindings_.push_back(DbType(val)); + return *this; + } + + Sql& Bind(double val) { + bindings_.push_back(DbType(val)); + return *this; + } + + Sql& Bind(std::vector val) { + bindings_.push_back(DbType(std::move(val))); + return *this; + } + + Sql& Bind(int pos, std::string val) { + binding_map_[pos] = DbType(std::move(val)); + return *this; + } + + Sql& Bind(int pos, int val) { + binding_map_[pos] = DbType(val); + return *this; + } + + Sql& Bind(int pos, double val) { + binding_map_[pos] = DbType(val); + return *this; + } + + Sql& Bind(int pos, std::vector val) { + binding_map_[pos] = DbType(std::move(val)); + return *this; + } + + Sql& Bind(std::string name, std::string val) { + binding_name_map_[std::move(name)] = DbType(std::move(val)); + return *this; + } + + Sql& Bind(std::string name, int val) { + binding_name_map_[std::move(name)] = DbType(val); + return *this; + } + + Sql& Bind(std::string name, double val) { + binding_name_map_[std::move(name)] = DbType(val); + return *this; + } + + Sql& Bind(std::string name, std::vector val) { + binding_name_map_[std::move(name)] = DbType(std::move(val)); + return *this; + } + + const std::vector& GetBindings() const { + return bindings_; + } + + const std::map& GetBindingMap() const { + return binding_map_; + } + + const std::map& GetBindingNameMap() const { + return binding_name_map_; + } + + const std::string& GetQuery() const { + return query_; + } + + private: + std::string query_; + std::vector bindings_; + std::map binding_map_; + std::map binding_name_map_; + }; + + class Result { + public: + Result() = default; + ~Result() { + if (stmt_) + sqlite3_finalize(stmt_); + } + + Result(const Result&) = delete; + Result& operator = (const Result&) = delete; + + Result(Result&& r) noexcept { + stmt_ = r.stmt_; + r.stmt_ = nullptr; + } + + Result& operator = (Result&& r) noexcept { + if (this != &r) { + if (stmt_) + sqlite3_finalize(stmt_); + stmt_ = r.stmt_; + r.stmt_ = nullptr; + } + + return *this; + } + + class Record { + public: + Record(const sqlite3_stmt* stmt) : stmt_(stmt) {} + + AutoDbType Get(int pos) const { + sqlite3_stmt* stmt = const_cast(stmt_); + int type = sqlite3_column_type(stmt, pos); + if (type == SQLITE_NULL) + throw std::runtime_error("invalid column"); + + DbType dbt; + if (type == SQLITE_TEXT) { + dbt = DbType(reinterpret_cast( + sqlite3_column_text(stmt, pos))); + } else if (type == SQLITE_INTEGER) { + dbt = DbType(sqlite3_column_int(stmt, pos)); + } else if (type == SQLITE_FLOAT) { + dbt = DbType(sqlite3_column_double(stmt, pos)); + } else if (type == SQLITE_BLOB) { + const unsigned char* val = reinterpret_cast( + sqlite3_column_blob(stmt, pos)); + int len = sqlite3_column_bytes(stmt, pos); + + if (!val || len < 0) { + throw std::runtime_error("invalid blob");; + } else { + dbt = DbType(std::vector(val, val + len)); + } + } else { + throw std::runtime_error("invalid column type"); + } + + return AutoDbType(dbt); + } + + template + auto Get() const { + std::tuple t; + int pos = 0; + for_>>([&] (auto i) { + std::get(t) = Get(pos++); + }); + + return t; + } + + private: + const sqlite3_stmt* stmt_; + }; + + class Iterator { + public: + Iterator(sqlite3_stmt* stmt) : stmt_(stmt) {} + + Record operator*() { return Record(stmt_); } + + bool operator != (const Iterator& rhs) const { + return stmt_ != rhs.stmt_; + } + + void operator ++() { + int r = sqlite3_step(stmt_); + if (r != SQLITE_ROW) + stmt_ = nullptr; + } + + private: + sqlite3_stmt* stmt_ = nullptr; + }; + + Iterator begin() const { + return Iterator(stmt_); + } + + Iterator end() const { + return Iterator(nullptr); + } + + explicit operator bool() { + if (stmt_ == nullptr) + return false; + return true; + } + + explicit operator int() { + if (db_ == nullptr) + return SQLITE_ERROR; + return sqlite3_errcode(db_); + } + + operator const char*() { + if (db_ == nullptr) + return ""; + return sqlite3_errmsg(db_); + } + + template + std::vector ToVector() { + if (stmt_ == nullptr) + return {}; + + std::vector vec; + for (const auto& rec : *this) { + T t; + t(rec); + vec.push_back(std::move(t)); + } + + return vec; + } + + template + std::list ToList() { + if (stmt_ == nullptr) + return {}; + + std::list l; + for (const auto& rec : *this) { + T t; + t(rec); + l.push_back(std::move(t)); + } + + return l; + } + + template + std::optional GetFirst() { + if (stmt_ == nullptr) + return std::nullopt; + for (const auto& rec : *this) { + T t; + t(rec); + return t; + } + + return std::nullopt; + } + + std::optional GetFirstRecord() { + if (stmt_ == nullptr) + return std::nullopt; + for (const auto& rec : *this) { + return rec; + } + + return std::nullopt; + } + + private: + friend class Database; + Result(sqlite3_stmt* stmt, sqlite3* db) : stmt_(stmt), db_(db) {} + + sqlite3_stmt* stmt_ = nullptr; + sqlite3* db_ = nullptr; + }; + + Database(std::string db, int flags) { + int r = sqlite3_open_v2(db.c_str(), &db_, flags, nullptr); + if (r != SQLITE_OK) + throw std::runtime_error("open failed"); + } + + Database(std::string db, int flags, std::function busy_handler) { + int r = sqlite3_open_v2(db.c_str(), &db_, flags, nullptr); + if (r != SQLITE_OK) + throw std::runtime_error("sqlite3_open_v2() failed"); + + busy_handler_ = std::move(busy_handler); + r = sqlite3_busy_handler(db_, [](void* data, int count) { + Database* pDb = static_cast(data); + if (pDb->busy_handler_(count)) + return 1; + return 0; + }, this); + + if (r != SQLITE_OK) { + sqlite3_close_v2(db_); + throw std::runtime_error("sqlite3_busy_handler() failed"); + } + } + + ~Database() { + if (db_) + sqlite3_close_v2(db_); + } + + Database() = default; + Database(const Database&) = delete; + Database& operator = (const Database&) = delete; + + Database(Database&& db) noexcept { + db_ = db.db_; + db.db_ = nullptr; + } + + explicit operator bool() { + if (db_ == nullptr) + return false; + return true; + } + + Database& operator = (Database&& db) noexcept { + if (this != &db) { + if (db_) + sqlite3_close_v2(db_); + db_ = db.db_; + db.db_ = nullptr; + } + + return *this; + } + + TransactionGuard CreateTransactionGuard() { + return TransactionGuard(db_); + } + + Result Exec(const Sql& sql) const { + if (!db_) + throw std::runtime_error("Not opened"); + + sqlite3_stmt* stmt = nullptr; + int r = sqlite3_prepare_v2(db_, sql.GetQuery().c_str(), + -1, &stmt, nullptr); + if (r != SQLITE_OK) { + return { nullptr, nullptr }; + } + + std::unique_ptr stmt_auto(stmt, + sqlite3_finalize); + int pos = 1; + for (const auto& i : sql.GetBindings()) { + Bind(pos++, i, stmt); + } + + for (const auto& i : sql.GetBindingMap()) { + Bind(i.first, i.second, stmt); + } + + for (const auto& i : sql.GetBindingNameMap()) { + int pos = sqlite3_bind_parameter_index(stmt, i.first.c_str()); + if (pos == 0) + throw std::runtime_error("Invalid binding"); + Bind(pos, i.second, stmt); + } + + r = sqlite3_step(stmt); + if (r != SQLITE_ROW && r != SQLITE_DONE) { + return { nullptr, db_ }; + } + + return { stmt_auto.release(), db_ }; + } + + private: + void Bind(int pos, const DbType& type, sqlite3_stmt* stmt) const { + int r; + if (const std::string* pstr = std::get_if(&type)) { + r = sqlite3_bind_text(stmt, pos, (*pstr).c_str(), -1, + SQLITE_TRANSIENT); + } else if (const int* pint = std::get_if(&type)) { + r = sqlite3_bind_int(stmt, pos, (*pint)); + } else if (const double* pdouble = std::get_if(&type)) { + r = sqlite3_bind_double(stmt, pos, (*pdouble)); + } else if (const std::vector* pvector = + std::get_if>(&type)) { + r = sqlite3_bind_blob(stmt, pos, (*pvector).data(), + (*pvector).size(), nullptr); + } else { + r = -1; + } + + if (r != SQLITE_OK) { + throw std::runtime_error("Invalid binding"); + } + } + + private: + sqlite3* db_ = nullptr; + std::function busy_handler_; +}; + +} // namespace tizen_base + +#endif // TIZEN_DATABASE_DATABASE_HPP_ diff --git a/tizen-database/tizen-database.pc.in b/tizen-database/tizen-database.pc.in new file mode 100644 index 0000000..82ee192 --- /dev/null +++ b/tizen-database/tizen-database.pc.in @@ -0,0 +1,11 @@ +# Wrapper class for sqlite3 + +prefix=@PREFIX@ +includedir=${prefix}/include + +Name: tizen-database +Description: Wrapper class for sqlite3 +Version: @VERSION@ +Requires: sqlite3 +Cflags: -I${includedir} -I${includedir}/tizen-database +cppflags: -I${includedir} -I${includedir}/tizen-database