Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 146 additions & 3 deletions drogon_ctl/create_model.cc
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,59 @@ bool drogon_ctl::ConvertMethod::shouldConvert(const std::string &tableName,
} // endif
}

/**
* @brief Try to add an auto-detected FK relationship to the list.
*
* Checks for duplicates against existing relationships, creates a
* Relationship object from the FK info, and appends it to the list.
* User-configured relationships always take priority.
*
* @param allRelationships The mutable vector of relationships.
* @param originalTable The table containing the FK column.
* @param fkColumn The FK column name.
* @param referencedTable The table referenced by the FK.
* @param referencedColumn The column referenced by the FK.
* @param normalizeNames If true, apply toLower() to table names.
*/
static void tryAddAutoRelationship(std::vector<Relationship> &allRelationships,
const std::string &originalTable,
const std::string &fkColumn,
const std::string &referencedTable,
const std::string &referencedColumn,
bool normalizeNames)
{
for (const auto &r : allRelationships)
{
if (r.originalKey() == fkColumn &&
r.targetTableName() == referencedTable)
{
return; // Already exists in user config
}
}
Json::Value relJson;
relJson["type"] = "has one";
relJson["original_table_name"] =
normalizeNames ? toLower(originalTable) : originalTable;
relJson["original_key"] = fkColumn;
relJson["target_table_name"] =
normalizeNames ? toLower(referencedTable) : referencedTable;
relJson["target_key"] = referencedColumn;
relJson["enable_reverse"] = true;
try
{
Relationship autoRel(relJson);
allRelationships.push_back(autoRel);
std::cout << " Auto-detected FK: " << originalTable << "."
<< fkColumn << " -> " << referencedTable << "."
<< referencedColumn << std::endl;
}
catch (const std::runtime_error &e)
{
std::cerr << "Warning: Could not create auto-relationship: " << e.what()
<< std::endl;
}
}

#if USE_POSTGRESQL
void create_model::createModelClassFromPG(
const std::string &path,
Expand All @@ -182,8 +235,9 @@ void create_model::createModelClassFromPG(
data["primaryKeyName"] = "";
data["dbName"] = dbname_;
data["rdbms"] = std::string("postgresql");
data["relationships"] = relationships;
data["convertMethods"] = convertMethods;
// Start with user-configured relationships (mutable copy)
std::vector<Relationship> allRelationships(relationships);
if (schema != "public")
{
data["schema"] = schema;
Expand Down Expand Up @@ -397,6 +451,42 @@ void create_model::createModelClassFromPG(
data["primaryKeyValNames"] = pkValNames;
}

// Auto-detect foreign key relationships from database schema
*client << "SELECT "
"kcu.column_name AS fk_column, "
"ccu.table_name AS referenced_table, "
"ccu.column_name AS referenced_column "
"FROM information_schema.key_column_usage kcu "
"JOIN information_schema.referential_constraints rc "
"ON kcu.constraint_name = rc.constraint_name "
"AND kcu.constraint_schema = rc.constraint_schema "
"JOIN information_schema.constraint_column_usage ccu "
"ON rc.unique_constraint_name = ccu.constraint_name "
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostgreSQL FK introspection join is incomplete: rc.unique_constraint_name = ccu.constraint_name should also be joined on the constraint schema (e.g., rc.unique_constraint_schema = ccu.constraint_schema). Without it, the query can mis-resolve referenced tables/columns when constraint names collide across schemas or return incorrect/multiple matches.

Suggested change
"ON rc.unique_constraint_name = ccu.constraint_name "
"ON rc.unique_constraint_name = ccu.constraint_name "
"AND rc.unique_constraint_schema = ccu.constraint_schema "

Copilot uses AI. Check for mistakes.
"AND rc.unique_constraint_schema = ccu.constraint_schema "
"WHERE kcu.table_name = $1 "
"AND kcu.table_schema = $2"
<< tableName << schema << Mode::Blocking >>
[&](bool isNull,
const std::string &fkColumn,
const std::string &referencedTable,
const std::string &referencedColumn) {
if (!isNull)
{
tryAddAutoRelationship(allRelationships,
tableName,
fkColumn,
referencedTable,
referencedColumn,
true);
}
} >>
[](const DrogonDbException &e) {
// FK detection is best-effort; don't fail if unsupported
std::cerr << "Note: FK auto-detection not available: "
<< e.base().what() << std::endl;
};

data["relationships"] = allRelationships;
data["columns"] = cols;
std::ofstream headerFile(path + "/" + className + ".h", std::ofstream::out);
std::ofstream sourceFile(path + "/" + className + ".cc",
Expand Down Expand Up @@ -467,8 +557,9 @@ void create_model::createModelClassFromMysql(
data["primaryKeyName"] = "";
data["dbName"] = dbname_;
data["rdbms"] = std::string("mysql");
data["relationships"] = relationships;
data["convertMethods"] = convertMethods;
// Start with user-configured relationships (mutable copy)
std::vector<Relationship> allRelationships(relationships);
std::vector<ColumnInfo> cols;
int i = 0;
*client << "desc `" + tableName + "`" << Mode::Blocking >>
Expand Down Expand Up @@ -593,6 +684,35 @@ void create_model::createModelClassFromMysql(
data["primaryKeyType"] = pkTypes;
data["primaryKeyValNames"] = pkValNames;
}

// Auto-detect foreign key relationships from MySQL schema
*client << "SELECT COLUMN_NAME, REFERENCED_TABLE_NAME, "
"REFERENCED_COLUMN_NAME "
"FROM information_schema.KEY_COLUMN_USAGE "
"WHERE TABLE_SCHEMA = DATABASE() "
"AND TABLE_NAME = ? "
"AND REFERENCED_TABLE_NAME IS NOT NULL"
<< tableName << Mode::Blocking >>
[&](bool isNull,
const std::string &fkColumn,
const std::string &referencedTable,
const std::string &referencedColumn) {
if (!isNull)
{
tryAddAutoRelationship(allRelationships,
tableName,
fkColumn,
referencedTable,
referencedColumn,
true);
}
} >>
[](const DrogonDbException &e) {
std::cerr << "Note: FK auto-detection not available: "
<< e.base().what() << std::endl;
};

data["relationships"] = allRelationships;
data["columns"] = cols;
std::ofstream headerFile(path + "/" + className + ".h", std::ofstream::out);
std::ofstream sourceFile(path + "/" + className + ".cc",
Expand Down Expand Up @@ -646,8 +766,9 @@ void create_model::createModelClassFromSqlite3(
data["primaryKeyName"] = "";
data["dbName"] = std::string("sqlite3");
data["rdbms"] = std::string("sqlite3");
data["relationships"] = relationships;
data["convertMethods"] = convertMethods;
// Start with user-configured relationships (mutable copy)
std::vector<Relationship> allRelationships(relationships);
std::vector<ColumnInfo> cols;
std::string sql = "PRAGMA table_info(" + tableName + ");";
*client << sql << Mode::Blocking >> [&](const Result &result) {
Expand Down Expand Up @@ -774,6 +895,28 @@ void create_model::createModelClassFromSqlite3(
data["primaryKeyType"] = pkTypes;
data["primaryKeyValNames"] = pkValNames;
}

// Auto-detect foreign key relationships from SQLite3 schema
std::string fkSql = "PRAGMA foreign_key_list(\"" + tableName + "\");";
*client << fkSql << Mode::Blocking >> [&](const Result &fkResult) {
for (auto &fkRow : fkResult)
{
auto referencedTable = fkRow["table"].as<std::string>();
auto fkColumn = fkRow["from"].as<std::string>();
auto referencedColumn = fkRow["to"].as<std::string>();
tryAddAutoRelationship(allRelationships,
tableName,
fkColumn,
referencedTable,
referencedColumn,
true);
}
} >> [](const DrogonDbException &e) {
std::cerr << "Note: FK auto-detection not available: "
<< e.base().what() << std::endl;
};

data["relationships"] = allRelationships;
data["columns"] = cols;
std::ofstream headerFile(path + "/" + className + ".h", std::ofstream::out);
std::ofstream sourceFile(path + "/" + className + ".cc",
Expand Down
69 changes: 69 additions & 0 deletions orm_lib/inc/drogon/orm/BaseBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,69 @@ struct Filter
std::string value;
};

/**
* @brief Represents a SQL JOIN clause.
*/
enum class JoinType
{
InnerJoin,
LeftJoin,
RightJoin,
FullJoin
};

inline std::string to_join_string(JoinType type)
{
switch (type)
{
case JoinType::InnerJoin:
return "INNER JOIN";
case JoinType::LeftJoin:
return "LEFT JOIN";
case JoinType::RightJoin:
return "RIGHT JOIN";
case JoinType::FullJoin:
return "FULL JOIN";
}
// Should never reach here
return "INNER JOIN";
}
Comment on lines +81 to +96
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default branch silently maps unknown values to INNER JOIN, which can hide bugs (e.g., if JoinType is extended or memory is corrupted). Prefer eliminating the default and letting compilers warn on non-exhaustive switches, or use an explicit failure path (e.g., unimplemented() / assertion) consistent with to_string(CompareOperator).

Copilot uses AI. Check for mistakes.

struct JoinClause
{
JoinType type;
std::string table;
std::string onLeft; // e.g. "users.id"
std::string onRight; // e.g. "posts.user_id"
};

/**
* @brief Validate that a string is a safe SQL identifier.
*
* Only allows alphanumeric characters, underscores, and dots
* (for table.column notation). This prevents SQL injection when
* building JOIN clauses from user-provided identifiers.
*
* @param identifier The identifier to validate.
* @return true if the identifier is safe to use in SQL.
*/
inline bool isValidSqlIdentifier(const std::string &identifier)
{
if (identifier.empty())
{
return false;
}
for (auto c : identifier)
{
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_' &&
c != '.')
{
return false;
}
}
return true;
}

// Forward declaration to be a friend
template <typename T, bool SelectAll, bool Single = false>
class TransformBuilder;
Expand All @@ -87,6 +150,7 @@ class BaseBuilder
std::string from_;
std::string columns_;
std::vector<Filter> filters_;
std::vector<JoinClause> joins_;
std::optional<std::uint64_t> limit_;
std::optional<std::uint64_t> offset_;
// The order is important; use vector<pair> instead of unordered_map and
Expand Down Expand Up @@ -122,6 +186,11 @@ class BaseBuilder
};

std::string sql = "select " + columns_ + " from " + from_;
for (const auto &join : joins_)
{
sql += " " + to_join_string(join.type) + " " + join.table + " ON " +
join.onLeft + " = " + join.onRight;
}
Comment on lines 188 to +193
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JOIN SQL is assembled via raw string concatenation of join.table, join.onLeft, and join.onRight. If any of these can originate from user-controlled inputs (directly or indirectly), this becomes an injection vector. Consider enforcing identifier validation/quoting at the point joins are added (e.g., in FilterBuilder::*Join()), so BaseBuilder can safely assume the join parts are sanitized.

Copilot uses AI. Check for mistakes.
if (!filters_.empty())
{
sql += " where " + filters_[0].column + " " +
Expand Down
51 changes: 51 additions & 0 deletions orm_lib/inc/drogon/orm/CoroMapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,54 @@ class CoroMapper : public Mapper<T>
return *this;
}

/**
* @brief Add an INNER JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return CoroMapper<T>& The CoroMapper itself.
*/
CoroMapper<T> &innerJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
Mapper<T>::innerJoin(table, onLeft, onRight);
return *this;
}

/**
* @brief Add a LEFT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return CoroMapper<T>& The CoroMapper itself.
*/
CoroMapper<T> &leftJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
Mapper<T>::leftJoin(table, onLeft, onRight);
return *this;
}

/**
* @brief Add a RIGHT JOIN clause to the query.
*
* @param table The table to join.
* @param onLeft The left side of ON (e.g. "users.id").
* @param onRight The right side of ON (e.g. "posts.user_id").
* @return CoroMapper<T>& The CoroMapper itself.
*/
CoroMapper<T> &rightJoin(const std::string &table,
const std::string &onLeft,
const std::string &onRight)
{
Mapper<T>::rightJoin(table, onLeft, onRight);
return *this;
}

// Read api for coroutines

inline internal::MapperAwaiter<std::vector<T>> findAll()
Expand All @@ -225,6 +273,7 @@ class CoroMapper : public Mapper<T>
ExceptPtrCallback &&errCallback) {
std::string sql = "select count(*) from ";
sql += T::tableName;
sql += this->joinString_;
if (criteria)
{
sql += " where ";
Expand All @@ -250,6 +299,7 @@ class CoroMapper : public Mapper<T>
ExceptPtrCallback &&errCallback) {
std::string sql = "select * from ";
sql += T::tableName;
sql += this->joinString_;
bool hasParameters = false;
if (criteria)
{
Expand Down Expand Up @@ -311,6 +361,7 @@ class CoroMapper : public Mapper<T>
ExceptPtrCallback &&errCallback) {
std::string sql = "select * from ";
sql += T::tableName;
sql += this->joinString_;
bool hasParameters = false;
if (criteria)
{
Expand Down
Loading
Loading