Skip to content

Commit 2db43f4

Browse files
committed
为 Drogon 框架添加 DuckDB 数据库支持,使用原生c api
1 parent 7abc38b commit 2db43f4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1992
-19374
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ CMakeSettings.json
4747
install
4848
trace.json
4949
.cache/
50+
CLAUDE.md

CMakeLists.txt

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ project(drogon)
44

55
message(STATUS "compiler: " ${CMAKE_CXX_COMPILER_ID})
66

7+
# Set C++20 as the minimum required standard
8+
set(CMAKE_CXX_STANDARD 20)
9+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
10+
set(CMAKE_CXX_EXTENSIONS OFF)
11+
712
option(BUILD_CTL "Build drogon_ctl" ON)
813
option(BUILD_EXAMPLES "Build examples" ON)
914
option(BUILD_ORM "Build orm" ON)
@@ -490,22 +495,16 @@ if (BUILD_DUCKDB)
490495
message(STATUS "Ok! We find DuckDB!")
491496
target_link_libraries(${PROJECT_NAME} PRIVATE ${DuckDB_LIBRARIES})
492497
target_include_directories(${PROJECT_NAME} PRIVATE ${DuckDB_INCLUDE_DIRS})
493-
add_subdirectory(orm_lib/src/duckdb_wrapper_impl/sqlite3_api_wrapper)
494498
set(DROGON_FOUND_DUCKDB TRUE)
495-
# 链接duckdb_wrapper_impl/sqlite3_api_wrapper的对象文件
496-
target_sources(${PROJECT_NAME} PRIVATE ${ALL_OBJECT_FILES})
497-
# set(DROGON_SOURCES ...)
498-
# set(private_headers ...)
499-
#include_directories(orm_lib/src/duckdb_wrapper_impl/inc)
499+
# 使用 duckdb_impl 目录下的 DuckDB C API 直接实现
500500
set(DROGON_SOURCES
501501
${DROGON_SOURCES}
502-
orm_lib/src/duckdb_wrapper_impl/DuckdbConnection.cc
503-
orm_lib/src/duckdb_wrapper_impl/DuckdbResultImpl.cc)
502+
orm_lib/src/duckdb_impl/DuckdbConnection.cc
503+
orm_lib/src/duckdb_impl/DuckdbResultImpl.cc)
504504
set(private_headers
505505
${private_headers}
506-
#orm_lib/src/duckdb_wrapper_impl/sqlite3_api_wrapper/inc/sqlite3_duck.h
507-
orm_lib/src/duckdb_wrapper_impl/DuckdbConnection.h
508-
orm_lib/src/duckdb_wrapper_impl/DuckdbResultImpl.h)
506+
orm_lib/src/duckdb_impl/DuckdbConnection.h
507+
orm_lib/src/duckdb_impl/DuckdbResultImpl.h)
509508
else (DuckDB_FOUND)
510509
message(STATUS "DuckDB was not found.")
511510
set(DROGON_FOUND_DUCKDB FALSE)
@@ -738,9 +737,9 @@ if (BUILD_TESTING)
738737
if (DROGON_FOUND_SQLite3)
739738
add_subdirectory(${PROJECT_SOURCE_DIR}/orm_lib/src/sqlite3_impl/test)
740739
endif (DROGON_FOUND_SQLite3)
741-
# Add DuckDB test
740+
# Add DuckDB test
742741
if (DROGON_FOUND_DUCKDB)
743-
add_subdirectory(${PROJECT_SOURCE_DIR}/orm_lib/src/duckdb_wrapper_impl/test)
742+
add_subdirectory(${PROJECT_SOURCE_DIR}/orm_lib/src/duckdb_impl/test)
744743
endif (DROGON_FOUND_DUCKDB)
745744
add_subdirectory(${PROJECT_SOURCE_DIR}/orm_lib/tests)
746745
endif (BUILD_TESTING)

drogon_ctl/create_model.cc

Lines changed: 298 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,225 @@ void create_model::createModelFromSqlite3(
814814
}
815815
#endif
816816

817+
// DuckDB support implementation [dq 2025-11-19]
818+
#if USE_DUCKDB
819+
void create_model::createModelClassFromDuckdb(
820+
const std::string &path,
821+
const DbClientPtr &client,
822+
const std::string &tableName,
823+
const Json::Value &restfulApiConfig,
824+
const std::vector<Relationship> &relationships,
825+
const std::vector<ConvertMethod> &convertMethods)
826+
{
827+
HttpViewData data;
828+
auto className = nameTransform(tableName, true);
829+
data["className"] = className;
830+
data["tableName"] = toLower(tableName);
831+
data["hasPrimaryKey"] = (int)0;
832+
data["primaryKeyName"] = "";
833+
data["dbName"] = std::string("duckdb");
834+
data["rdbms"] = std::string("duckdb");
835+
data["relationships"] = relationships;
836+
data["convertMethods"] = convertMethods;
837+
std::vector<ColumnInfo> cols;
838+
839+
// 使用 PRAGMA table_info 查询表结构(DuckDB 兼容 SQLite)
840+
std::string sql = "PRAGMA table_info(" + tableName + ");";
841+
*client << sql << Mode::Blocking >> [&](const Result &result) {
842+
size_t index = 0;
843+
for (auto &row : result)
844+
{
845+
bool notnull = row["notnull"].as<bool>();
846+
bool primary = row["pk"].as<int>();
847+
auto type = row["type"].as<std::string>();
848+
std::transform(type.begin(),
849+
type.end(),
850+
type.begin(),
851+
[](unsigned char c) { return tolower(c); });
852+
ColumnInfo info;
853+
info.index_ = index++;
854+
info.dbType_ = "duckdb"; // 标记为 DuckDB 类型
855+
info.colName_ = row["name"].as<std::string>();
856+
info.colTypeName_ = nameTransform(info.colName_, true);
857+
info.colValName_ = nameTransform(info.colName_, false);
858+
info.notNull_ = notnull;
859+
info.colDatabaseType_ = type;
860+
info.isPrimaryKey_ = primary;
861+
862+
// DuckDB 自动增长检测
863+
if (primary)
864+
{
865+
// 查询表的 DDL 来检测 GENERATED BY DEFAULT AS IDENTITY
866+
*client << "SELECT sql FROM duckdb_tables() WHERE table_name=?"
867+
<< tableName << Mode::Blocking >>
868+
[&](bool isNull, std::string sql) {
869+
if (!isNull)
870+
{
871+
std::transform(sql.begin(),
872+
sql.end(),
873+
sql.begin(),
874+
[](unsigned char c) {
875+
return tolower(c);
876+
});
877+
// DuckDB 使用 GENERATED BY DEFAULT AS IDENTITY
878+
if (sql.find("generated") != std::string::npos &&
879+
sql.find("identity") != std::string::npos)
880+
{
881+
info.isAutoVal_ = true;
882+
}
883+
}
884+
} >>
885+
[](const DrogonDbException &e) {
886+
// 忽略错误
887+
};
888+
}
889+
890+
auto defaultVal = row["dflt_value"].as<std::string>();
891+
if (!defaultVal.empty())
892+
{
893+
info.hasDefaultVal_ = true;
894+
}
895+
896+
// DuckDB 类型映射到 C++ 类型
897+
if (type.find("bigint") != std::string::npos)
898+
{
899+
info.colType_ = "int64_t";
900+
info.colLength_ = 8;
901+
}
902+
else if (type.find("int") != std::string::npos)
903+
{
904+
// DuckDB 的 INTEGER 默认是 64 位
905+
info.colType_ = "int64_t";
906+
info.colLength_ = 8;
907+
}
908+
else if (type.find("smallint") != std::string::npos)
909+
{
910+
info.colType_ = "int16_t";
911+
info.colLength_ = 2;
912+
}
913+
else if (type.find("tinyint") != std::string::npos)
914+
{
915+
info.colType_ = "int8_t";
916+
info.colLength_ = 1;
917+
}
918+
else if (type.find("varchar") != std::string::npos ||
919+
type == "text" || type == "string")
920+
{
921+
info.colType_ = "std::string";
922+
}
923+
else if (type.find("double") != std::string::npos)
924+
{
925+
info.colType_ = "double";
926+
info.colLength_ = sizeof(double);
927+
}
928+
else if (type == "real" || type == "float")
929+
{
930+
info.colType_ = "float";
931+
info.colLength_ = sizeof(float);
932+
}
933+
else if (type.find("decimal") != std::string::npos ||
934+
type.find("numeric") != std::string::npos)
935+
{
936+
// 使用字符串存储精确小数,避免浮点误差
937+
info.colType_ = "std::string";
938+
}
939+
else if (type == "boolean" || type == "bool")
940+
{
941+
info.colType_ = "bool";
942+
info.colLength_ = 1;
943+
}
944+
else if (type == "blob")
945+
{
946+
info.colType_ = "std::vector<char>";
947+
}
948+
else if (type == "date" || type == "datetime" ||
949+
type.find("timestamp") != std::string::npos)
950+
{
951+
info.colType_ = "::trantor::Date";
952+
}
953+
else
954+
{
955+
// 未知类型默认使用字符串
956+
info.colType_ = "std::string";
957+
}
958+
cols.push_back(std::move(info));
959+
}
960+
} >> [](const DrogonDbException &e) {
961+
std::cerr << e.base().what() << std::endl;
962+
exit(1);
963+
};
964+
965+
// 处理主键
966+
std::vector<std::string> pkNames, pkTypes, pkValNames;
967+
for (auto const &col : cols)
968+
{
969+
if (col.isPrimaryKey_)
970+
{
971+
pkNames.push_back(col.colName_);
972+
pkTypes.push_back(col.colType_);
973+
pkValNames.push_back(nameTransform(col.colName_, false));
974+
}
975+
}
976+
data["hasPrimaryKey"] = (int)pkNames.size();
977+
if (pkNames.size() == 1)
978+
{
979+
data["primaryKeyName"] = pkNames[0];
980+
data["primaryKeyType"] = pkTypes[0];
981+
}
982+
else if (pkNames.size() > 1)
983+
{
984+
// 复合主键不支持自动增长
985+
for (auto &col : cols)
986+
{
987+
col.isAutoVal_ = false;
988+
}
989+
data["primaryKeyName"] = pkNames;
990+
data["primaryKeyType"] = pkTypes;
991+
data["primaryKeyValNames"] = pkValNames;
992+
}
993+
994+
data["columns"] = cols;
995+
std::ofstream headerFile(path + "/" + className + ".h", std::ofstream::out);
996+
std::ofstream sourceFile(path + "/" + className + ".cc",
997+
std::ofstream::out);
998+
auto templ = DrTemplateBase::newTemplate("model_h.csp");
999+
headerFile << templ->genText(data);
1000+
templ = DrTemplateBase::newTemplate("model_cc.csp");
1001+
sourceFile << templ->genText(data);
1002+
createRestfulAPIController(data, restfulApiConfig);
1003+
}
1004+
1005+
void create_model::createModelFromDuckdb(
1006+
const std::string &path,
1007+
const DbClientPtr &client,
1008+
const Json::Value &restfulApiConfig,
1009+
std::map<std::string, std::vector<Relationship>> &relationships,
1010+
std::map<std::string, std::vector<ConvertMethod>> &convertMethods)
1011+
{
1012+
// 查询所有用户表(排除系统表)
1013+
*client << "SELECT table_name FROM information_schema.tables "
1014+
"WHERE table_schema = 'main' AND table_type = 'BASE TABLE' "
1015+
"ORDER BY table_name"
1016+
<< Mode::Blocking >>
1017+
[&](bool isNull, std::string &&tableName) mutable {
1018+
if (!isNull)
1019+
{
1020+
std::cout << "table name:" << tableName << std::endl;
1021+
createModelClassFromDuckdb(path,
1022+
client,
1023+
tableName,
1024+
restfulApiConfig,
1025+
relationships[tableName],
1026+
convertMethods[tableName]);
1027+
}
1028+
} >>
1029+
[](const DrogonDbException &e) {
1030+
std::cerr << e.base().what() << std::endl;
1031+
exit(1);
1032+
};
1033+
}
1034+
#endif
1035+
8171036
void create_model::createModel(const std::string &path,
8181037
const Json::Value &config,
8191038
const std::string &singleModelName)
@@ -1119,6 +1338,82 @@ void create_model::createModel(const std::string &path,
11191338
std::cerr << "Drogon does not support Sqlite3, please install Sqlite3 "
11201339
"development environment before installing drogon"
11211340
<< std::endl;
1341+
#endif
1342+
}
1343+
// DuckDB database type handling [dq 2025-11-19]
1344+
else if (dbType == "duckdb")
1345+
{
1346+
#if USE_DUCKDB
1347+
auto filename = config.get("filename", "").asString();
1348+
if (filename == "")
1349+
{
1350+
std::cerr << "Please configure filename in " << path
1351+
<< "/model.json " << std::endl;
1352+
exit(1);
1353+
}
1354+
std::string connStr = "filename=" + escapeConnString(filename);
1355+
DbClientPtr client =
1356+
drogon::orm::DbClient::newDuckDbClient(connStr, 1);
1357+
std::cout << "Connect to DuckDB..." << std::endl;
1358+
if (forceOverwrite_)
1359+
{
1360+
std::this_thread::sleep_for(1s);
1361+
}
1362+
else
1363+
{
1364+
std::cout << "Source files in the " << path
1365+
<< " folder will be overwritten, continue(y/n)?\n";
1366+
auto in = getchar();
1367+
(void)getchar(); // get the return key
1368+
if (in != 'Y' && in != 'y')
1369+
{
1370+
std::cout << "Abort!" << std::endl;
1371+
exit(0);
1372+
}
1373+
}
1374+
1375+
if (singleModelName.empty())
1376+
{
1377+
auto tables = config["tables"];
1378+
if (!tables || tables.size() == 0)
1379+
createModelFromDuckdb(path,
1380+
client,
1381+
restfulApiConfig,
1382+
relationships,
1383+
convertMethods);
1384+
else
1385+
{
1386+
for (int i = 0; i < (int)tables.size(); ++i)
1387+
{
1388+
auto tableName = tables[i].asString();
1389+
std::transform(tableName.begin(),
1390+
tableName.end(),
1391+
tableName.begin(),
1392+
[](unsigned char c) { return tolower(c); });
1393+
std::cout << "table name:" << tableName << std::endl;
1394+
createModelClassFromDuckdb(path,
1395+
client,
1396+
tableName,
1397+
restfulApiConfig,
1398+
relationships[tableName],
1399+
convertMethods[tableName]);
1400+
}
1401+
}
1402+
}
1403+
else
1404+
{
1405+
createModelClassFromDuckdb(path,
1406+
client,
1407+
singleModelName,
1408+
restfulApiConfig,
1409+
relationships[singleModelName],
1410+
convertMethods[singleModelName]);
1411+
}
1412+
1413+
#else
1414+
std::cerr << "Drogon does not support DuckDB, please install DuckDB "
1415+
"development environment before installing drogon"
1416+
<< std::endl;
11221417
#endif
11231418
}
11241419
else if (dbType == "no dbms")
@@ -1129,7 +1424,9 @@ void create_model::createModel(const std::string &path,
11291424
}
11301425
else
11311426
{
1132-
std::cerr << "Does not support " << dbType << std::endl;
1427+
std::cerr << "Does not support " << dbType
1428+
<< ". Supported databases: postgresql, mysql, sqlite3, duckdb"
1429+
<< std::endl;
11331430
exit(1);
11341431
}
11351432
}

drogon_ctl/create_model.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,22 @@ class create_model : public DrObject<create_model>, public CommandHandler
424424
const Json::Value &restfulApiConfig,
425425
std::map<std::string, std::vector<Relationship>> &relationships,
426426
std::map<std::string, std::vector<ConvertMethod>> &convertMethod);
427+
#endif
428+
// DuckDB model generation support [dq 2025-11-19]
429+
#if USE_DUCKDB
430+
void createModelClassFromDuckdb(
431+
const std::string &path,
432+
const DbClientPtr &client,
433+
const std::string &tableName,
434+
const Json::Value &restfulApiConfig,
435+
const std::vector<Relationship> &relationships,
436+
const std::vector<ConvertMethod> &convertMethods);
437+
void createModelFromDuckdb(
438+
const std::string &path,
439+
const DbClientPtr &client,
440+
const Json::Value &restfulApiConfig,
441+
std::map<std::string, std::vector<Relationship>> &relationships,
442+
std::map<std::string, std::vector<ConvertMethod>> &convertMethods);
427443
#endif
428444
void createRestfulAPIController(const DrTemplateData &tableInfo,
429445
const Json::Value &restfulApiConfig);

0 commit comments

Comments
 (0)