Skip to content

Commit 796dfe4

Browse files
authored
orm: add explicit JOIN support (INNER, LEFT, RIGHT, FULL OUTER) (fix #21635) (#26400)
1 parent 3e331d4 commit 796dfe4

7 files changed

Lines changed: 522 additions & 0 deletions

File tree

‎vlib/orm/orm.v‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,32 @@ pub enum OrderType {
8989
desc
9090
}
9191

92+
// JoinType represents the type of SQL JOIN operation
93+
pub enum JoinType {
94+
inner // INNER JOIN - returns only matching rows
95+
left // LEFT JOIN - returns all left rows, NULL for non-matching right
96+
right // RIGHT JOIN - returns all right rows, NULL for non-matching left
97+
full_outer // FULL OUTER JOIN - returns all rows from both tables
98+
}
99+
100+
fn (jt JoinType) to_str() string {
101+
return match jt {
102+
.inner { 'INNER JOIN' }
103+
.left { 'LEFT JOIN' }
104+
.right { 'RIGHT JOIN' }
105+
.full_outer { 'FULL OUTER JOIN' }
106+
}
107+
}
108+
109+
// JoinConfig holds configuration for a JOIN clause in a SELECT query
110+
pub struct JoinConfig {
111+
pub mut:
112+
kind JoinType
113+
table Table
114+
on_left_col string // Column from main table (e.g., 'user_id')
115+
on_right_col string // Column from joined table (e.g., 'id')
116+
}
117+
92118
pub enum SQLDialect {
93119
default
94120
mysql
@@ -182,6 +208,7 @@ pub mut:
182208
// has_offset - Add an offset to the result
183209
// fields - Fields to select
184210
// types - Types to select
211+
// joins - JOIN clauses for this query
185212
pub struct SelectConfig {
186213
pub mut:
187214
table Table
@@ -196,6 +223,7 @@ pub mut:
196223
has_distinct bool
197224
fields []string
198225
types []int
226+
joins []JoinConfig // JOIN clauses for this query
199227
}
200228

201229
// Interfaces gets called from the backend and can be implemented
@@ -367,6 +395,13 @@ pub fn orm_select_gen(cfg SelectConfig, q string, num bool, qm string, start_pos
367395

368396
str += ' FROM ${q}${cfg.table.name}${q}'
369397

398+
// Generate JOIN clauses
399+
for join in cfg.joins {
400+
str += ' ${join.kind.to_str()} ${q}${join.table.name}${q}'
401+
str += ' ON ${q}${cfg.table.name}${q}.${q}${join.on_left_col}${q}'
402+
str += ' = ${q}${join.table.name}${q}.${q}${join.on_right_col}${q}'
403+
}
404+
370405
mut c := start_pos
371406

372407
if cfg.has_where {

‎vlib/orm/orm_join_test.v‎

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// vtest retry: 3
2+
// vtest build: present_sqlite3? && !windows && !sanitize-memory-clang
3+
import db.sqlite
4+
5+
// Department table for testing JOINs - using dept_id to avoid column name conflicts
6+
struct Department {
7+
dept_id int @[primary; sql: serial]
8+
dept_name string
9+
}
10+
11+
// User table with a foreign key reference to Department
12+
struct User {
13+
user_id int @[primary; sql: serial]
14+
user_name string
15+
department_id int
16+
}
17+
18+
fn test_inner_join() {
19+
db := sqlite.connect(':memory:') or { panic(err) }
20+
21+
// Create tables
22+
sql db {
23+
create table Department
24+
create table User
25+
}!
26+
27+
// Insert departments
28+
engineering := Department{
29+
dept_name: 'Engineering'
30+
}
31+
sales := Department{
32+
dept_name: 'Sales'
33+
}
34+
sql db {
35+
insert engineering into Department
36+
insert sales into Department
37+
}!
38+
39+
// Insert users
40+
alice := User{
41+
user_name: 'Alice'
42+
department_id: 1
43+
}
44+
bob := User{
45+
user_name: 'Bob'
46+
department_id: 2
47+
}
48+
charlie := User{
49+
user_name: 'Charlie'
50+
department_id: 1
51+
}
52+
sql db {
53+
insert alice into User
54+
insert bob into User
55+
insert charlie into User
56+
}!
57+
58+
// Test basic INNER JOIN
59+
users := sql db {
60+
select from User
61+
join Department on User.department_id == Department.dept_id
62+
}!
63+
64+
assert users.len == 3
65+
}
66+
67+
fn test_inner_join_with_where() {
68+
db := sqlite.connect(':memory:') or { panic(err) }
69+
70+
// Create tables
71+
sql db {
72+
create table Department
73+
create table User
74+
}!
75+
76+
// Insert departments
77+
engineering := Department{
78+
dept_name: 'Engineering'
79+
}
80+
sales := Department{
81+
dept_name: 'Sales'
82+
}
83+
sql db {
84+
insert engineering into Department
85+
insert sales into Department
86+
}!
87+
88+
// Insert users
89+
alice := User{
90+
user_name: 'Alice'
91+
department_id: 1
92+
}
93+
bob := User{
94+
user_name: 'Bob'
95+
department_id: 2
96+
}
97+
charlie := User{
98+
user_name: 'Charlie'
99+
department_id: 1
100+
}
101+
sql db {
102+
insert alice into User
103+
insert bob into User
104+
insert charlie into User
105+
}!
106+
107+
// Test INNER JOIN with WHERE clause - use simple field name (not Table.field)
108+
engineering_users := sql db {
109+
select from User
110+
join Department on User.department_id == Department.dept_id where department_id == 1
111+
}!
112+
113+
assert engineering_users.len == 2
114+
assert engineering_users[0].user_name == 'Alice' || engineering_users[0].user_name == 'Charlie'
115+
}
116+
117+
fn test_left_join() {
118+
db := sqlite.connect(':memory:') or { panic(err) }
119+
120+
// Create tables
121+
sql db {
122+
create table Department
123+
create table User
124+
}!
125+
126+
// Insert departments
127+
engineering := Department{
128+
dept_name: 'Engineering'
129+
}
130+
sql db {
131+
insert engineering into Department
132+
}!
133+
134+
// Insert users - one with a department, one without (orphan)
135+
alice := User{
136+
user_name: 'Alice'
137+
department_id: 1
138+
}
139+
bob := User{
140+
user_name: 'Bob'
141+
department_id: 999 // No matching department
142+
}
143+
sql db {
144+
insert alice into User
145+
insert bob into User
146+
}!
147+
148+
// Test LEFT JOIN - should return all users, even those without matching department
149+
users := sql db {
150+
select from User
151+
left join Department on User.department_id == Department.dept_id
152+
}!
153+
154+
// Both users should be returned since it's a LEFT JOIN
155+
assert users.len == 2
156+
}

‎vlib/v/ast/ast.v‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2288,6 +2288,24 @@ pub mut:
22882288
end_comments []Comment
22892289
}
22902290

2291+
// JoinKind represents the type of SQL JOIN operation
2292+
pub enum JoinKind {
2293+
inner // INNER JOIN - returns only matching rows
2294+
left // LEFT JOIN - returns all left rows, NULL for non-matching right
2295+
right // RIGHT JOIN - returns all right rows, NULL for non-matching left
2296+
full_outer // FULL OUTER JOIN - returns all rows from both tables
2297+
}
2298+
2299+
// JoinClause represents a JOIN clause in an SQL SELECT query
2300+
pub struct JoinClause {
2301+
pub:
2302+
kind JoinKind
2303+
pos token.Pos
2304+
pub mut:
2305+
table_expr TypeNode // The table being joined (e.g., Department in `join Department`)
2306+
on_expr Expr // The ON condition (e.g., `User.dept_id == Department.id`)
2307+
}
2308+
22912309
pub struct SqlExpr {
22922310
pub:
22932311
is_count bool
@@ -2315,6 +2333,7 @@ pub mut:
23152333
fields []StructField
23162334
sub_structs map[int]SqlExpr
23172335
or_expr OrExpr
2336+
joins []JoinClause // JOIN clauses for this query
23182337
}
23192338

23202339
pub struct NodeError {

0 commit comments

Comments
 (0)