Skip to content

Commit 0d86cc1

Browse files
Pierre-Luc Gagnéclaude
andcommitted
feat: add NATURAL JOIN, JOIN USING, LATERAL JOIN, and STRAIGHT_JOIN
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7110ff6 commit 0d86cc1

3 files changed

Lines changed: 346 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1010

1111
### Added
1212

13+
- `natural_join<T>()`, `natural_left_join<T>()`, `natural_right_join<T>()``NATURAL [LEFT|RIGHT] JOIN` with no ON/USING clause
14+
- `inner_join_using<T, Cols...>()`, `left_join_using<T, Cols...>()`, `right_join_using<T, Cols...>()`, `full_join_using<T, Cols...>()``JOIN ... USING (col1, col2, ...)` with one or more column descriptors
15+
- `lateral_join(sql, alias)`, `left_lateral_join(sql, alias)``[LEFT] JOIN LATERAL (subquery) AS alias` (MySQL 8.0+)
16+
- `lateral_join_on(sql, alias, cond)`, `left_lateral_join_on(sql, alias, cond)` — lateral join variants with an explicit `ON` condition
17+
- `straight_join<T, LeftCol, RightCol>()`, `straight_join_on<T>(cond)``STRAIGHT_JOIN` MySQL optimizer hint forcing left-to-right join evaluation order
1318
- `not_regexp<Col>(pattern)``col NOT REGEXP 'pattern'`
1419
- `rlike<Col>(pattern)``col RLIKE 'pattern'` (MySQL synonym for `REGEXP`)
1520
- `not_rlike<Col>(pattern)``col NOT RLIKE 'pattern'`

lib/include/ds_mysql/sql_dql.hpp

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3179,6 +3179,165 @@ struct select_query_builder {
31793179
std::move(on_cond));
31803180
}
31813181

3182+
// natural_join — NATURAL JOIN (implicit column matching, no ON/USING clause)
3183+
template <typename RightTable>
3184+
[[nodiscard]] select_query_builder natural_join() const& {
3185+
auto copy = *this;
3186+
copy.joins_ += " NATURAL JOIN " + std::string(table_name_for<RightTable>::value().to_string_view());
3187+
return copy;
3188+
}
3189+
template <typename RightTable>
3190+
[[nodiscard]] select_query_builder natural_join() && {
3191+
joins_ += " NATURAL JOIN " + std::string(table_name_for<RightTable>::value().to_string_view());
3192+
return std::move(*this);
3193+
}
3194+
3195+
// natural_left_join — NATURAL LEFT JOIN
3196+
template <typename RightTable>
3197+
[[nodiscard]] select_query_builder natural_left_join() const& {
3198+
auto copy = *this;
3199+
copy.joins_ += " NATURAL LEFT JOIN " + std::string(table_name_for<RightTable>::value().to_string_view());
3200+
return copy;
3201+
}
3202+
template <typename RightTable>
3203+
[[nodiscard]] select_query_builder natural_left_join() && {
3204+
joins_ += " NATURAL LEFT JOIN " + std::string(table_name_for<RightTable>::value().to_string_view());
3205+
return std::move(*this);
3206+
}
3207+
3208+
// natural_right_join — NATURAL RIGHT JOIN
3209+
template <typename RightTable>
3210+
[[nodiscard]] select_query_builder natural_right_join() const& {
3211+
auto copy = *this;
3212+
copy.joins_ += " NATURAL RIGHT JOIN " + std::string(table_name_for<RightTable>::value().to_string_view());
3213+
return copy;
3214+
}
3215+
template <typename RightTable>
3216+
[[nodiscard]] select_query_builder natural_right_join() && {
3217+
joins_ += " NATURAL RIGHT JOIN " + std::string(table_name_for<RightTable>::value().to_string_view());
3218+
return std::move(*this);
3219+
}
3220+
3221+
// join_using — JOIN ... USING (col1, col2, ...) variants
3222+
template <typename RightTable, ColumnDescriptor... UsingCols>
3223+
[[nodiscard]] select_query_builder inner_join_using() const& {
3224+
return add_join_using("INNER JOIN", table_name_for<RightTable>::value().to_string_view(),
3225+
build_using_cols_string<UsingCols...>());
3226+
}
3227+
template <typename RightTable, ColumnDescriptor... UsingCols>
3228+
[[nodiscard]] select_query_builder inner_join_using() && {
3229+
return std::move(*this).add_join_using("INNER JOIN", table_name_for<RightTable>::value().to_string_view(),
3230+
build_using_cols_string<UsingCols...>());
3231+
}
3232+
3233+
template <typename RightTable, ColumnDescriptor... UsingCols>
3234+
[[nodiscard]] select_query_builder left_join_using() const& {
3235+
return add_join_using("LEFT JOIN", table_name_for<RightTable>::value().to_string_view(),
3236+
build_using_cols_string<UsingCols...>());
3237+
}
3238+
template <typename RightTable, ColumnDescriptor... UsingCols>
3239+
[[nodiscard]] select_query_builder left_join_using() && {
3240+
return std::move(*this).add_join_using("LEFT JOIN", table_name_for<RightTable>::value().to_string_view(),
3241+
build_using_cols_string<UsingCols...>());
3242+
}
3243+
3244+
template <typename RightTable, ColumnDescriptor... UsingCols>
3245+
[[nodiscard]] select_query_builder right_join_using() const& {
3246+
return add_join_using("RIGHT JOIN", table_name_for<RightTable>::value().to_string_view(),
3247+
build_using_cols_string<UsingCols...>());
3248+
}
3249+
template <typename RightTable, ColumnDescriptor... UsingCols>
3250+
[[nodiscard]] select_query_builder right_join_using() && {
3251+
return std::move(*this).add_join_using("RIGHT JOIN", table_name_for<RightTable>::value().to_string_view(),
3252+
build_using_cols_string<UsingCols...>());
3253+
}
3254+
3255+
template <typename RightTable, ColumnDescriptor... UsingCols>
3256+
[[nodiscard]] select_query_builder full_join_using() const& {
3257+
return add_join_using("FULL OUTER JOIN", table_name_for<RightTable>::value().to_string_view(),
3258+
build_using_cols_string<UsingCols...>());
3259+
}
3260+
template <typename RightTable, ColumnDescriptor... UsingCols>
3261+
[[nodiscard]] select_query_builder full_join_using() && {
3262+
return std::move(*this).add_join_using("FULL OUTER JOIN", table_name_for<RightTable>::value().to_string_view(),
3263+
build_using_cols_string<UsingCols...>());
3264+
}
3265+
3266+
// lateral_join — JOIN LATERAL (subquery) AS alias (MySQL 8.0+)
3267+
// Pass a pre-built SQL string as the subquery (e.g. inner_query.build_sql()).
3268+
[[nodiscard]] select_query_builder lateral_join(std::string subquery_sql, std::string_view alias) const& {
3269+
auto copy = *this;
3270+
copy.joins_ += " JOIN LATERAL (" + std::move(subquery_sql) + ") AS " + std::string(alias);
3271+
return copy;
3272+
}
3273+
[[nodiscard]] select_query_builder lateral_join(std::string subquery_sql, std::string_view alias) && {
3274+
joins_ += " JOIN LATERAL (" + std::move(subquery_sql) + ") AS " + std::string(alias);
3275+
return std::move(*this);
3276+
}
3277+
3278+
// left_lateral_join — LEFT JOIN LATERAL (subquery) AS alias (MySQL 8.0+)
3279+
[[nodiscard]] select_query_builder left_lateral_join(std::string subquery_sql, std::string_view alias) const& {
3280+
auto copy = *this;
3281+
copy.joins_ += " LEFT JOIN LATERAL (" + std::move(subquery_sql) + ") AS " + std::string(alias);
3282+
return copy;
3283+
}
3284+
[[nodiscard]] select_query_builder left_lateral_join(std::string subquery_sql, std::string_view alias) && {
3285+
joins_ += " LEFT JOIN LATERAL (" + std::move(subquery_sql) + ") AS " + std::string(alias);
3286+
return std::move(*this);
3287+
}
3288+
3289+
// lateral_join_on — JOIN LATERAL (subquery) AS alias ON condition
3290+
[[nodiscard]] select_query_builder lateral_join_on(std::string subquery_sql, std::string_view alias,
3291+
where_condition on_cond) const& {
3292+
auto copy = *this;
3293+
copy.joins_ += " JOIN LATERAL (" + std::move(subquery_sql) + ") AS " + std::string(alias) + " ON " +
3294+
on_cond.build_sql();
3295+
return copy;
3296+
}
3297+
[[nodiscard]] select_query_builder lateral_join_on(std::string subquery_sql, std::string_view alias,
3298+
where_condition on_cond) && {
3299+
joins_ += " JOIN LATERAL (" + std::move(subquery_sql) + ") AS " + std::string(alias) + " ON " +
3300+
on_cond.build_sql();
3301+
return std::move(*this);
3302+
}
3303+
3304+
// left_lateral_join_on — LEFT JOIN LATERAL (subquery) AS alias ON condition
3305+
[[nodiscard]] select_query_builder left_lateral_join_on(std::string subquery_sql, std::string_view alias,
3306+
where_condition on_cond) const& {
3307+
auto copy = *this;
3308+
copy.joins_ += " LEFT JOIN LATERAL (" + std::move(subquery_sql) + ") AS " + std::string(alias) + " ON " +
3309+
on_cond.build_sql();
3310+
return copy;
3311+
}
3312+
[[nodiscard]] select_query_builder left_lateral_join_on(std::string subquery_sql, std::string_view alias,
3313+
where_condition on_cond) && {
3314+
joins_ += " LEFT JOIN LATERAL (" + std::move(subquery_sql) + ") AS " + std::string(alias) + " ON " +
3315+
on_cond.build_sql();
3316+
return std::move(*this);
3317+
}
3318+
3319+
// straight_join — STRAIGHT_JOIN (MySQL optimizer hint: forces left-to-right join order)
3320+
template <typename RightTable, ColumnDescriptor LeftCol, ColumnDescriptor RightCol>
3321+
[[nodiscard]] select_query_builder straight_join() const& {
3322+
return add_join("STRAIGHT_JOIN", table_name_for<RightTable>::value().to_string_view(),
3323+
qualified_col_name<LeftCol>(), qualified_col_name<RightCol>());
3324+
}
3325+
template <typename RightTable, ColumnDescriptor LeftCol, ColumnDescriptor RightCol>
3326+
[[nodiscard]] select_query_builder straight_join() && {
3327+
return std::move(*this).add_join("STRAIGHT_JOIN", table_name_for<RightTable>::value().to_string_view(),
3328+
qualified_col_name<LeftCol>(), qualified_col_name<RightCol>());
3329+
}
3330+
3331+
template <typename RightTable>
3332+
[[nodiscard]] select_query_builder straight_join_on(where_condition on_cond) const& {
3333+
return add_join_on("STRAIGHT_JOIN", table_name_for<RightTable>::value().to_string_view(), std::move(on_cond));
3334+
}
3335+
template <typename RightTable>
3336+
[[nodiscard]] select_query_builder straight_join_on(where_condition on_cond) && {
3337+
return std::move(*this).add_join_on("STRAIGHT_JOIN", table_name_for<RightTable>::value().to_string_view(),
3338+
std::move(on_cond));
3339+
}
3340+
31823341
// ---- Terminal ----
31833342

31843343
[[nodiscard]] std::string build_sql() const {
@@ -3304,6 +3463,28 @@ struct select_query_builder {
33043463
return std::move(*this);
33053464
}
33063465

3466+
[[nodiscard]] select_query_builder add_join_using(std::string_view join_type, std::string_view right_table,
3467+
std::string using_cols) const& {
3468+
auto copy = *this;
3469+
copy.joins_ +=
3470+
" " + std::string(join_type) + " " + std::string(right_table) + " USING (" + std::move(using_cols) + ")";
3471+
return copy;
3472+
}
3473+
[[nodiscard]] select_query_builder add_join_using(std::string_view join_type, std::string_view right_table,
3474+
std::string using_cols) && {
3475+
joins_ +=
3476+
" " + std::string(join_type) + " " + std::string(right_table) + " USING (" + std::move(using_cols) + ")";
3477+
return std::move(*this);
3478+
}
3479+
3480+
template <ColumnDescriptor... UsingCols>
3481+
static std::string build_using_cols_string() {
3482+
std::string s;
3483+
bool first = true;
3484+
((s += (first ? "" : ", "), s += std::string(column_traits<UsingCols>::column_name()), first = false), ...);
3485+
return s;
3486+
}
3487+
33073488
// Private helper: builds a comma-separated GROUP BY column name string.
33083489
template <ColumnDescriptor... GroupCols>
33093490
static std::string build_group_by_string() {

tests/unit/test_dql.cpp

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2331,6 +2331,166 @@ suite<"DQL JOIN Features"> dql_join_features_suite = [] {
23312331
};
23322332
};
23332333

2334+
// ===================================================================
2335+
// NATURAL JOIN / JOIN USING / LATERAL JOIN / STRAIGHT_JOIN
2336+
// ===================================================================
2337+
2338+
suite<"DQL Extended JOIN Types"> dql_extended_join_suite = [] {
2339+
// ---- NATURAL JOIN ----
2340+
2341+
"natural_join — generates NATURAL JOIN"_test = [] {
2342+
auto const sql = select<ext_product::sku, ext_category::label>()
2343+
.from<joined<ext_product>>()
2344+
.natural_join<ext_category>()
2345+
.build_sql();
2346+
expect(sql == "SELECT sku, label FROM ext_product NATURAL JOIN ext_category"s) << sql;
2347+
};
2348+
2349+
"natural_left_join — generates NATURAL LEFT JOIN"_test = [] {
2350+
auto const sql = select<ext_product::sku, ext_category::label>()
2351+
.from<joined<ext_product>>()
2352+
.natural_left_join<ext_category>()
2353+
.build_sql();
2354+
expect(sql == "SELECT sku, label FROM ext_product NATURAL LEFT JOIN ext_category"s) << sql;
2355+
};
2356+
2357+
"natural_right_join — generates NATURAL RIGHT JOIN"_test = [] {
2358+
auto const sql = select<ext_product::sku, ext_category::label>()
2359+
.from<joined<ext_product>>()
2360+
.natural_right_join<ext_category>()
2361+
.build_sql();
2362+
expect(sql == "SELECT sku, label FROM ext_product NATURAL RIGHT JOIN ext_category"s) << sql;
2363+
};
2364+
2365+
"natural_join chained with WHERE — natural join composes with WHERE"_test = [] {
2366+
auto const sql = select<ext_product::sku>()
2367+
.from<joined<ext_product>>()
2368+
.natural_join<ext_category>()
2369+
.where(is_not_null<ext_product::name>())
2370+
.build_sql();
2371+
expect(sql == "SELECT sku FROM ext_product NATURAL JOIN ext_category WHERE name IS NOT NULL"s) << sql;
2372+
};
2373+
2374+
// ---- JOIN ... USING ----
2375+
2376+
"inner_join_using single col — generates INNER JOIN ... USING (col)"_test = [] {
2377+
auto const sql = select<ext_product::sku, ext_category::label>()
2378+
.from<joined<ext_product>>()
2379+
.inner_join_using<ext_category, ext_product::category_id>()
2380+
.build_sql();
2381+
expect(sql == "SELECT sku, label FROM ext_product INNER JOIN ext_category USING (category_id)"s) << sql;
2382+
};
2383+
2384+
"left_join_using — generates LEFT JOIN ... USING (col)"_test = [] {
2385+
auto const sql = select<ext_product::sku>()
2386+
.from<joined<ext_product>>()
2387+
.left_join_using<ext_category, ext_product::category_id>()
2388+
.build_sql();
2389+
expect(sql == "SELECT sku FROM ext_product LEFT JOIN ext_category USING (category_id)"s) << sql;
2390+
};
2391+
2392+
"right_join_using — generates RIGHT JOIN ... USING (col)"_test = [] {
2393+
auto const sql = select<ext_product::sku>()
2394+
.from<joined<ext_product>>()
2395+
.right_join_using<ext_category, ext_product::category_id>()
2396+
.build_sql();
2397+
expect(sql == "SELECT sku FROM ext_product RIGHT JOIN ext_category USING (category_id)"s) << sql;
2398+
};
2399+
2400+
"full_join_using — generates FULL OUTER JOIN ... USING (col)"_test = [] {
2401+
auto const sql = select<ext_product::sku>()
2402+
.from<joined<ext_product>>()
2403+
.full_join_using<ext_category, ext_product::category_id>()
2404+
.build_sql();
2405+
expect(sql == "SELECT sku FROM ext_product FULL OUTER JOIN ext_category USING (category_id)"s) << sql;
2406+
};
2407+
2408+
"inner_join_using multiple cols — generates INNER JOIN ... USING (col1, col2)"_test = [] {
2409+
auto const sql = select<ext_product::sku>()
2410+
.from<joined<ext_product>>()
2411+
.inner_join_using<ext_category, ext_product::category_id, ext_product::id>()
2412+
.build_sql();
2413+
expect(sql == "SELECT sku FROM ext_product INNER JOIN ext_category USING (category_id, id)"s) << sql;
2414+
};
2415+
2416+
// ---- LATERAL JOIN ----
2417+
2418+
"lateral_join — generates JOIN LATERAL (subquery) AS alias"_test = [] {
2419+
auto const sub = select<ext_product::sku>().from<ext_product>().build_sql();
2420+
auto const sql = select<ext_product::sku>()
2421+
.from<joined<ext_product>>()
2422+
.lateral_join(sub, "lat")
2423+
.build_sql();
2424+
expect(sql ==
2425+
"SELECT sku FROM ext_product JOIN LATERAL (SELECT sku FROM ext_product) AS lat"s)
2426+
<< sql;
2427+
};
2428+
2429+
"left_lateral_join — generates LEFT JOIN LATERAL (subquery) AS alias"_test = [] {
2430+
auto const sub = select<ext_product::sku>().from<ext_product>().build_sql();
2431+
auto const sql = select<ext_product::sku>()
2432+
.from<joined<ext_product>>()
2433+
.left_lateral_join(sub, "lat")
2434+
.build_sql();
2435+
expect(sql ==
2436+
"SELECT sku FROM ext_product LEFT JOIN LATERAL (SELECT sku FROM ext_product) AS lat"s)
2437+
<< sql;
2438+
};
2439+
2440+
"lateral_join_on — generates JOIN LATERAL (subquery) AS alias ON condition"_test = [] {
2441+
auto const sub = select<ext_product::sku>().from<ext_product>().build_sql();
2442+
auto const on = col_ref<ext_product::id> == 1u;
2443+
auto const sql = select<ext_product::sku>()
2444+
.from<joined<ext_product>>()
2445+
.lateral_join_on(sub, "lat", std::move(on))
2446+
.build_sql();
2447+
expect(sql ==
2448+
"SELECT sku FROM ext_product JOIN LATERAL (SELECT sku FROM ext_product) AS lat ON id = 1"s)
2449+
<< sql;
2450+
};
2451+
2452+
"left_lateral_join_on — generates LEFT JOIN LATERAL (subquery) AS alias ON condition"_test = [] {
2453+
auto const sub = select<ext_product::sku>().from<ext_product>().build_sql();
2454+
auto const on = col_ref<ext_product::id> == 2u;
2455+
auto const sql = select<ext_product::sku>()
2456+
.from<joined<ext_product>>()
2457+
.left_lateral_join_on(sub, "lat", std::move(on))
2458+
.build_sql();
2459+
expect(sql ==
2460+
"SELECT sku FROM ext_product LEFT JOIN LATERAL (SELECT sku FROM ext_product) AS lat ON id = 2"s)
2461+
<< sql;
2462+
};
2463+
2464+
// ---- STRAIGHT_JOIN ----
2465+
2466+
"straight_join — generates STRAIGHT_JOIN ... ON col1 = col2"_test = [] {
2467+
auto const sql = select<ext_product::sku, ext_category::label>()
2468+
.from<joined<ext_product>>()
2469+
.straight_join<ext_category, ext_product::category_id, ext_category::id>()
2470+
.build_sql();
2471+
expect(sql == "SELECT sku, label FROM ext_product STRAIGHT_JOIN ext_category ON category_id = id"s) << sql;
2472+
};
2473+
2474+
"straight_join with qualified col descriptors — generates table-qualified ON"_test = [] {
2475+
auto const sql = select<ext_product::sku, ext_category::label>()
2476+
.from<joined<ext_product>>()
2477+
.straight_join<ext_category, col<ext_product, 1>, col<ext_category, 0>>()
2478+
.build_sql();
2479+
expect(sql ==
2480+
"SELECT sku, label FROM ext_product STRAIGHT_JOIN ext_category ON ext_product.category_id = ext_category.id"s)
2481+
<< sql;
2482+
};
2483+
2484+
"straight_join_on — generates STRAIGHT_JOIN ... ON compound condition"_test = [] {
2485+
auto const on = col_ref<ext_product::category_id> == 5u;
2486+
auto const sql = select<ext_product::sku>()
2487+
.from<joined<ext_product>>()
2488+
.straight_join_on<ext_category>(std::move(on))
2489+
.build_sql();
2490+
expect(sql == "SELECT sku FROM ext_product STRAIGHT_JOIN ext_category ON category_id = 5"s) << sql;
2491+
};
2492+
};
2493+
23342494
// ===================================================================
23352495
// Derived table (subquery in FROM)
23362496
// ===================================================================

0 commit comments

Comments
 (0)