v2 / vlib / v / checker / orm.v
1658 lines · 1490 sloc · 51.73 KB · d5578efae67ef01ab3a63866d429b7dacf6c237d
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
3module checker
4
5import v.ast
6import v.token
7import v.util
8
9type ORMExpr = ast.SqlExpr | ast.SqlStmt
10
11enum SqlQueryDataContext {
12 where_
13 set_
14}
15
16fn (mut c Checker) sql_query_data_expr(mut node ast.SqlQueryDataExpr) ast.Type {
17 query_data_typ := c.table.find_type('orm.QueryData')
18 if query_data_typ == 0 {
19 return ast.void_type
20 }
21 for mut item in node.items {
22 c.check_sql_query_data_item(mut item)
23 }
24 node.typ = query_data_typ
25 return query_data_typ
26}
27
28fn (mut c Checker) check_sql_query_data_item(mut item ast.SqlQueryDataItem) {
29 match mut item {
30 ast.SqlQueryDataLeaf {
31 c.check_sql_query_data_leaf(item)
32 }
33 ast.SqlQueryDataIf {
34 for mut branch in item.branches {
35 if branch.cond !is ast.EmptyExpr {
36 mut cond := branch.cond
37 c.expr(mut cond)
38 branch.cond = cond
39 }
40 for mut branch_item in branch.items {
41 c.check_sql_query_data_item(mut branch_item)
42 }
43 }
44 }
45 }
46}
47
48fn (mut c Checker) check_sql_query_data_leaf(node ast.SqlQueryDataLeaf) {
49 mut expr := node.expr
50 expr_ := expr.remove_par()
51 if expr_ is ast.InfixExpr {
52 if !is_sql_query_data_op(expr_.op) {
53 c.orm_error('dynamic ORM items must use comparison operators', expr_.pos)
54 return
55 }
56 if !is_sql_query_data_field_candidate(expr_.left) {
57 c.orm_error('left side of a dynamic ORM item must be a field name', expr_.left.pos())
58 }
59 is_nil_comparison := expr_.right is ast.Nil && expr_.op in [.eq, .ne]
60 if expr_.op !in [.key_is, .not_is] && !is_nil_comparison {
61 mut rhs_expr := expr_.right
62 c.expr(mut rhs_expr)
63 }
64 } else {
65 c.orm_error('dynamic ORM items must be comparison expressions', node.pos)
66 }
67}
68
69fn is_sql_query_data_op(op token.Kind) bool {
70 return op in [.eq, .ne, .gt, .lt, .ge, .le, .key_like, .key_ilike, .key_in, .not_in, .key_is,
71 .not_is]
72}
73
74fn is_sql_query_data_field_candidate(expr ast.Expr) bool {
75 return match expr {
76 ast.Ident { true }
77 ast.SelectorExpr { is_sql_query_data_field_candidate(expr.expr) }
78 ast.ParExpr { is_sql_query_data_field_candidate(expr.expr) }
79 else { false }
80 }
81}
82
83fn sql_query_data_field_name(expr ast.Expr) string {
84 return match expr {
85 ast.Ident { expr.name }
86 ast.SelectorExpr { '${sql_query_data_field_name(expr.expr)}.${expr.field_name}' }
87 ast.ParExpr { sql_query_data_field_name(expr.expr) }
88 else { '' }
89 }
90}
91
92fn (mut c Checker) resolve_sql_query_data_expr(expr ast.Expr) !ast.SqlQueryDataExpr {
93 mut current := expr
94 for {
95 current = current.remove_par()
96 mut next_expr := current
97 mut has_next_expr := false
98 match current {
99 ast.SqlQueryDataExpr {
100 return current as ast.SqlQueryDataExpr
101 }
102 ast.Ident {
103 obj := current.obj
104 match obj {
105 ast.Var {
106 if obj.is_mut {
107 return error('dynamic ORM expressions must use an immutable query-data block alias')
108 }
109 next_expr = obj.expr
110 has_next_expr = true
111 }
112 ast.ConstField {
113 next_expr = obj.expr
114 has_next_expr = true
115 }
116 else {}
117 }
118 }
119 else {
120 return error('dynamic ORM expressions must use a query-data block or immutable alias to one')
121 }
122 }
123
124 if has_next_expr {
125 current = next_expr
126 continue
127 }
128 return error('dynamic ORM expressions must use a query-data block or immutable alias to one')
129 }
130 return error('dynamic ORM expressions must use a query-data block or immutable alias to one')
131}
132
133fn (mut c Checker) check_dynamic_sql_query_data(expr ast.Expr, table_sym &ast.TypeSymbol,
134 fields []ast.StructField, context SqlQueryDataContext) bool {
135 mut resolved := c.resolve_sql_query_data_expr(expr) or {
136 c.orm_error(err.msg(), expr.pos())
137 return false
138 }
139 field_names := fields.map(it.name)
140 return c.check_dynamic_sql_query_data_items(mut resolved.items, table_sym, fields, field_names,
141 context)
142}
143
144fn (mut c Checker) check_dynamic_sql_query_data_items(mut items []ast.SqlQueryDataItem, table_sym &ast.TypeSymbol,
145 fields []ast.StructField, field_names []string, context SqlQueryDataContext) bool {
146 mut ok := true
147 for mut item in items {
148 match item {
149 ast.SqlQueryDataLeaf {
150 mut expr := item.expr
151 expr_ := expr.remove_par()
152 if expr_ is ast.InfixExpr {
153 field_name := sql_query_data_field_name(expr_.left)
154 if field_name == '' {
155 c.orm_error('left side of a dynamic ORM item must be a field name',
156 expr_.left.pos())
157 ok = false
158 continue
159 }
160 matched_fields := fields.filter(it.name == field_name)
161 if matched_fields.len == 0 {
162 c.orm_error(util.new_suggestion(field_name, field_names).say('`${table_sym.name}` structure has no field with name `${field_name}`'),
163 expr_.left.pos())
164 ok = false
165 continue
166 }
167 field := matched_fields[0]
168 match context {
169 .where_ {
170 if expr_.op in [.and, .logical_or] {
171 c.orm_error('dynamic ORM `where` items must use a single comparison expression',
172 expr_.pos)
173 ok = false
174 continue
175 }
176 if !is_sql_query_data_op(expr_.op) {
177 c.orm_error('dynamic ORM `where` items must use comparison operators',
178 expr_.pos)
179 ok = false
180 continue
181 }
182 mut where_expr := item.expr
183 c.expr(mut where_expr)
184 c.check_expr_has_no_fn_calls_with_non_orm_return_type(&where_expr)
185 c.check_where_expr_has_no_pointless_exprs(table_sym, field_names,
186 &where_expr)
187 item.expr = where_expr
188 }
189 .set_ {
190 if expr_.op != .eq {
191 c.orm_error('dynamic ORM `set` items must use `==`', expr_.pos)
192 ok = false
193 continue
194 }
195 for attr in field.attrs {
196 if attr.name == 'fkey' {
197 c.orm_error("`${field_name}` is a foreign column of `${table_sym.name}`, it can't update here",
198 expr_.pos)
199 ok = false
200 break
201 }
202 }
203 mut set_expr := item.expr
204 old_expected_type := c.expected_type
205 c.expected_type = field.typ
206 c.expr(mut set_expr)
207 c.expected_type = old_expected_type
208 item.expr = set_expr
209 }
210 }
211 } else {
212 ok = false
213 continue
214 }
215 }
216 ast.SqlQueryDataIf {
217 for mut branch in item.branches {
218 if branch.cond !is ast.EmptyExpr {
219 mut cond := branch.cond
220 c.expr(mut cond)
221 branch.cond = cond
222 }
223 if !c.check_dynamic_sql_query_data_items(mut branch.items, table_sym, fields,
224 field_names, context) {
225 ok = false
226 }
227 }
228 }
229 }
230 }
231 return ok
232}
233
234fn (mut c Checker) sql_expr(mut node ast.SqlExpr) ast.Type {
235 c.inside_sql = true
236 defer {
237 c.inside_sql = false
238 }
239
240 if !c.check_db_expr(mut node.db_expr) {
241 return ast.void_type
242 }
243
244 c.resolve_orm_table_expr_type(mut node.table_expr)
245
246 // To avoid panics while working with `table_expr`,
247 // it is necessary to check if its type exists.
248 if !c.ensure_type_exists(node.table_expr.typ, node.pos) {
249 return ast.void_type
250 }
251
252 // Keep the SQL expression type aligned with the concretized ORM table type.
253 resolved_node_typ := c.unwrap_generic(node.typ)
254 if resolved_node_typ != node.typ {
255 node.typ = resolved_node_typ
256 }
257
258 table_type := node.table_expr.typ
259 table_sym := c.table.sym(table_type)
260
261 if !c.check_orm_table_expr_type(node.table_expr) {
262 return ast.void_type
263 }
264
265 old_ts := c.cur_orm_ts
266 c.cur_orm_ts = *table_sym
267 defer {
268 c.cur_orm_ts = old_ts
269 }
270
271 info := table_sym.info as ast.Struct
272 mut fields := c.fetch_and_check_orm_fields(info, node.table_expr.pos, table_sym.name)
273 non_primitive_fields := c.get_orm_non_primitive_fields(fields)
274 mut sub_structs := map[string]ast.SqlExpr{}
275
276 mut has_primary := false
277 mut primary_field := ast.StructField{}
278
279 for field in fields {
280 field_typ, field_sym := c.get_non_array_type(field.typ)
281 if field_sym.kind == .struct && (field_typ.idx() == node.table_expr.typ.idx()
282 || c.check_recursive_structs(field_sym, table_sym.name)) {
283 c.orm_error('invalid recursive struct `${field_sym.name}`', field.pos)
284 return ast.void_type
285 }
286
287 if field.attrs.contains('primary') {
288 if has_primary {
289 c.orm_error('a struct can only have one primary key', field.pos)
290 }
291 has_primary = true
292 primary_field = field
293 }
294 }
295
296 for field in non_primitive_fields {
297 field_sym := c.table.sym(field.typ.clear_flag(.option))
298 if field_sym.kind == .array && !has_primary {
299 c.orm_error('a struct that has a field that holds an array must have a primary key',
300 field.pos)
301 }
302
303 c.check_orm_non_primitive_struct_field_attrs(field)
304
305 foreign_typ := c.get_field_foreign_table_type(field)
306
307 // Get foreign struct's primary key field
308 foreign_sym := c.table.sym(foreign_typ)
309 if foreign_sym.info !is ast.Struct {
310 continue
311 }
312 foreign_info := foreign_sym.info as ast.Struct
313
314 // Find primary key field in foreign struct
315 mut foreign_primary_field := ast.StructField{}
316 mut foreign_has_primary := false
317 for f in foreign_info.fields {
318 if f.attrs.contains('primary') {
319 foreign_primary_field = f
320 foreign_has_primary = true
321 break
322 }
323 }
324
325 // Require foreign struct to have a primary key for relationship
326 if !foreign_has_primary {
327 c.orm_error('struct `${foreign_sym.name}` used as ORM sub-struct field `${field.name}` must have a `@[primary]` field, or use `@[sql: \'-\']` to skip this field',
328 field.pos)
329 continue
330 }
331
332 mut subquery_expr := ast.SqlExpr{
333 inserted_var: field.name
334 pos: node.pos
335 has_where: true
336 where_expr: ast.None{}
337 typ: field.typ.clear_flag(.option).set_flag(.result)
338 scope: c.fn_scope
339 db_expr: node.db_expr
340 table_expr: ast.TypeNode{
341 pos: node.table_expr.pos
342 typ: foreign_typ
343 }
344 is_generated: true
345 }
346
347 tmp_inside_sql := c.inside_sql
348 c.sql_expr(mut subquery_expr)
349 c.inside_sql = tmp_inside_sql
350
351 subquery_expr.where_expr = ast.InfixExpr{
352 op: .eq
353 pos: subquery_expr.pos
354 left: ast.Ident{
355 language: .v
356 tok_kind: .eq
357 scope: c.fn_scope
358 obj: ast.Var{}
359 mod: 'main'
360 name: foreign_primary_field.name
361 is_mut: false
362 kind: .unresolved
363 info: ast.IdentVar{}
364 }
365 right: ast.Ident{
366 language: .c
367 mod: 'main'
368 tok_kind: .eq
369 obj: ast.Var{}
370 is_mut: false
371 scope: c.fn_scope
372 info: ast.IdentVar{
373 typ: foreign_primary_field.typ
374 }
375 }
376 left_type: foreign_primary_field.typ
377 right_type: foreign_primary_field.typ
378 auto_locked: ''
379 or_block: ast.OrExpr{}
380 }
381
382 if field_sym.kind == .array {
383 mut where_expr := subquery_expr.where_expr
384 if mut where_expr is ast.InfixExpr {
385 where_expr.left_type = primary_field.typ
386 where_expr.right_type = primary_field.typ
387
388 mut left := where_expr.left
389 if mut left is ast.Ident {
390 left.name = primary_field.name
391 }
392
393 mut right := where_expr.right
394 if mut right is ast.Ident {
395 mut right_info := right.info
396 if mut right_info is ast.IdentVar {
397 right_info.typ = primary_field.typ
398 }
399 }
400 }
401 }
402
403 sub_structs[field.name] = subquery_expr
404 }
405
406 field_names := fields.map(it.name)
407 mut selected_fields := fields.clone()
408 if node.aggregate_kind == .none && node.requested_fields.len > 0 {
409 selected_fields = c.resolve_orm_selected_fields(node.requested_fields, fields, table_sym) or {
410 return ast.void_type
411 }
412 if has_primary {
413 selected_field_names := selected_fields.map(it.name)
414 for selected_field in selected_fields {
415 selected_field_type := c.table.final_type(selected_field.typ.clear_flag(.option))
416 if c.table.sym(selected_field_type).kind == .array
417 && primary_field.name !in selected_field_names {
418 c.orm_error('selecting array field `${selected_field.name}` requires selecting primary field `${primary_field.name}` too',
419 node.pos)
420 return ast.void_type
421 }
422 }
423 }
424 }
425 if node.aggregate_kind != .none {
426 node.sub_structs = map[string]ast.SqlExpr{}
427 if node.aggregate_kind == .count {
428 node.fields = [
429 ast.StructField{
430 typ: ast.int_type
431 },
432 ]
433 node.aggregate_field_type = ast.int_type
434 node.typ = ast.int_type.set_flag(.result)
435 } else {
436 aggregate_field := c.check_orm_aggregate_field(node.aggregate_kind,
437 node.aggregate_field, fields, table_sym.name, node.pos) or { return ast.void_type }
438 node.aggregate_field_type = aggregate_field.typ
439 node.fields = [
440 aggregate_field,
441 ]
442 node.typ =
443 c.orm_aggregate_return_type(node.aggregate_kind, aggregate_field.typ).set_flag(.result)
444 }
445 } else {
446 node.fields = selected_fields
447 node.sub_structs = sub_structs.move()
448 }
449
450 if node.has_where && !node.is_dynamic {
451 c.expr(mut node.where_expr)
452 c.check_expr_has_no_fn_calls_with_non_orm_return_type(&node.where_expr)
453 c.check_where_expr_has_no_pointless_exprs(table_sym, field_names, &node.where_expr)
454 } else if node.has_where && node.is_dynamic {
455 c.expr(mut node.where_expr)
456 if !c.check_dynamic_sql_query_data(node.where_expr, table_sym, fields, .where_) {
457 return ast.void_type
458 }
459 }
460
461 // Check JOIN clauses
462 for mut join in node.joins {
463 if !c.check_orm_join_clause(mut join, table_sym) {
464 return ast.void_type
465 }
466 }
467
468 if node.has_order {
469 if mut node.order_expr is ast.Ident {
470 order_ident_name := node.order_expr.name
471
472 if !table_sym.has_field(order_ident_name) {
473 c.orm_error(util.new_suggestion(order_ident_name, field_names).say('`${table_sym.name}` structure has no field with name `${order_ident_name}`'),
474 node.order_expr.pos)
475 return ast.void_type
476 }
477 } else {
478 c.orm_error("expected `${table_sym.name}` structure's field", node.order_expr.pos())
479 return ast.void_type
480 }
481
482 c.expr(mut node.order_expr)
483 }
484
485 if node.has_limit {
486 c.expr(mut node.limit_expr)
487 c.check_sql_value_expr_is_comptime_with_natural_number_or_expr_with_int_type(mut node.limit_expr,
488 'limit')
489 }
490
491 if node.has_offset {
492 c.expr(mut node.offset_expr)
493 c.check_sql_value_expr_is_comptime_with_natural_number_or_expr_with_int_type(mut node.offset_expr,
494 'offset')
495 }
496 c.expr(mut node.db_expr)
497 if node.is_insert {
498 node.typ = ast.int_type.set_flag(.result)
499 }
500 last_cur_or_expr := c.cur_or_expr
501 c.cur_or_expr = &node.or_expr
502 c.check_orm_or_expr(mut node)
503 c.cur_or_expr = last_cur_or_expr
504
505 return node.typ.clear_flag(.result)
506}
507
508fn (mut c Checker) sql_stmt(mut node ast.SqlStmt) ast.Type {
509 if !c.check_db_expr(mut node.db_expr) {
510 return ast.void_type
511 }
512 node.db_expr_type = c.table.unaliased_type(c.expr(mut node.db_expr))
513
514 for mut line in node.lines {
515 c.sql_stmt_line(mut line)
516 }
517 last_cur_or_expr := c.cur_or_expr
518 c.cur_or_expr = &node.or_expr
519 c.check_orm_or_expr(mut node)
520 c.cur_or_expr = last_cur_or_expr
521
522 return ast.void_type
523}
524
525fn (mut c Checker) sql_stmt_line(mut node ast.SqlStmtLine) ast.Type {
526 c.inside_sql = true
527 defer {
528 c.inside_sql = false
529 }
530
531 c.resolve_orm_table_expr_type(mut node.table_expr)
532
533 // To avoid panics while working with `table_expr`,
534 // it is necessary to check if its type exists.
535 if !c.ensure_type_exists(node.table_expr.typ, node.pos) {
536 return ast.void_type
537 }
538 table_type := node.table_expr.typ
539 table_sym := c.table.sym(table_type)
540
541 if !c.check_orm_table_expr_type(node.table_expr) {
542 return ast.void_type
543 }
544
545 old_ts := c.cur_orm_ts
546 c.cur_orm_ts = *table_sym
547 defer {
548 c.cur_orm_ts = old_ts
549 }
550
551 inserting_object_name := node.object_var
552
553 if node.kind in [.insert, .upsert] && !node.is_generated {
554 inserting_object := node.scope.find(inserting_object_name) or {
555 c.error('undefined ident: `${inserting_object_name}`', node.pos)
556 return ast.void_type
557 }
558 mut inserting_object_type := inserting_object.typ
559 mut is_array_insert := false
560
561 if inserting_object_type.is_ptr() {
562 inserting_object_type = inserting_object.typ.deref()
563 }
564
565 resolved_object_type := c.unwrap_generic(inserting_object_type)
566 if resolved_object_type != inserting_object_type {
567 inserting_object_type = resolved_object_type
568 }
569
570 insert_sym := c.table.sym(inserting_object_type)
571 if insert_sym.kind == .array {
572 if node.kind == .upsert {
573 c.orm_error('upsert currently does not support arrays', node.pos)
574 return ast.void_type
575 }
576 is_array_insert = true
577 elem_type := insert_sym.array_info().elem_type
578 if elem_type.is_ptr() {
579 c.orm_error('bulk ${node.kind} currently supports only arrays of `${table_sym.name}` values',
580 node.pos)
581 return ast.void_type
582 }
583 inserting_object_type = c.unwrap_generic(elem_type)
584 if inserting_object_type != node.table_expr.typ {
585 c.orm_error('bulk ${node.kind} currently supports only arrays of `${table_sym.name}` values',
586 node.pos)
587 return ast.void_type
588 }
589 }
590
591 if inserting_object_type != node.table_expr.typ
592 && !c.table.sumtype_has_variant(inserting_object_type, node.table_expr.typ, false) {
593 table_name := table_sym.name
594 inserting_type_name := c.table.sym(inserting_object_type).name
595
596 c.error('cannot use `${inserting_type_name}` as `${table_name}`', node.pos)
597 return ast.void_type
598 }
599 node.is_array_insert = is_array_insert
600 }
601
602 if table_sym.info !is ast.Struct {
603 c.error('unknown type `${table_sym.name}`', node.pos)
604 return ast.void_type
605 }
606
607 info := table_sym.info as ast.Struct
608 mut fields := c.fetch_and_check_orm_fields(info, node.table_expr.pos, table_sym.name)
609
610 mut insert_fields := []ast.StructField{cap: fields.len}
611 for field in fields {
612 c.check_orm_struct_field_attrs(node, field)
613 // Preserve SQL NULL/default handling for omitted reference fields instead of
614 // inserting the V zero value and violating foreign key constraints.
615 if field.attrs.contains('references')
616 && c.check_field_of_inserting_struct_is_uninitialized(node, field.name) {
617 continue
618 }
619 insert_fields << field
620 }
621 fields = insert_fields.clone()
622
623 mut sub_structs := map[string]ast.SqlStmtLine{}
624 non_primitive_fields := c.get_orm_non_primitive_fields(fields)
625
626 if node.is_array_insert && non_primitive_fields.len > 0 {
627 for field in non_primitive_fields {
628 c.orm_error('bulk ${node.kind} currently supports only primitive, enum, and time.Time fields',
629 field.pos)
630 }
631 return ast.void_type
632 }
633
634 for field in non_primitive_fields {
635 field_typ, field_sym := c.get_non_array_type(field.typ)
636 if field_sym.kind == .struct && (field_typ.idx() == node.table_expr.typ.idx()
637 || c.check_recursive_structs(field_sym, table_sym.name)) {
638 c.orm_error('invalid recursive struct `${field_sym.name}`', field.pos)
639 return ast.void_type
640 }
641
642 // Delete an uninitialized struct from fields and skip adding the current field
643 // to sub structs to skip inserting an empty struct in the related table.
644 if c.check_field_of_inserting_struct_is_uninitialized(node, field.name) {
645 fields.delete(fields.index(field))
646 continue
647 }
648
649 c.check_orm_non_primitive_struct_field_attrs(field)
650
651 foreign_typ := c.get_field_foreign_table_type(field)
652
653 mut subquery_expr := ast.SqlStmtLine{
654 pos: node.pos
655 kind: node.kind
656 table_expr: ast.TypeNode{
657 pos: node.table_expr.pos
658 typ: foreign_typ
659 }
660 object_var: field.name
661 is_generated: true
662 }
663
664 tmp_inside_sql := c.inside_sql
665 c.sql_stmt_line(mut subquery_expr)
666 c.inside_sql = tmp_inside_sql
667 sub_structs[field.name] = subquery_expr
668 }
669
670 node.fields = fields
671 node.sub_structs = sub_structs.move()
672
673 if node.kind == .upsert {
674 for field in non_primitive_fields {
675 field_typ, field_sym := c.get_non_array_type(field.typ)
676 if field_sym.kind == .struct && c.table.sym(field_typ).name == 'time.Time' {
677 continue
678 }
679 c.orm_error('upsert currently supports only primitive, enum, and time.Time fields',
680 field.pos)
681 }
682 }
683
684 for i, column in node.updated_columns {
685 updated_fields := node.fields.filter(it.name == column)
686
687 if updated_fields.len == 0 {
688 c.orm_error('type `${table_sym.name}` has no field named `${column}`', node.pos)
689 continue
690 }
691
692 field := updated_fields.first()
693 for attr in field.attrs {
694 if attr.name == 'fkey' {
695 c.orm_error("`${column}` is a foreign column of `${table_sym.name}`, it can't update here",
696 node.pos)
697 break
698 }
699 }
700 node.updated_columns[i] = c.fetch_field_name(field)
701 }
702
703 if node.kind == .update {
704 if node.is_dynamic {
705 c.expr(mut node.update_data_expr)
706 if !c.check_dynamic_sql_query_data(node.update_data_expr, table_sym, node.fields, .set_) {
707 return ast.void_type
708 }
709 } else if c.check_orm_array_update(mut node, table_sym) {
710 } else {
711 for i, mut expr in node.update_exprs {
712 column := node.updated_columns[i]
713 old_expected_type := c.expected_type
714 if field := c.get_orm_field_by_column_name(node.fields, column) {
715 c.expected_type = field.typ
716 }
717 c.expr(mut expr)
718 c.expected_type = old_expected_type
719 }
720 }
721 }
722
723 if node.where_expr !is ast.EmptyExpr && !node.is_array_update {
724 c.expr(mut node.where_expr)
725 }
726
727 return ast.void_type
728}
729
730fn (mut c Checker) check_orm_array_update(mut node ast.SqlStmtLine, table_sym &ast.TypeSymbol) bool {
731 if node.update_exprs.len == 0 || node.where_expr !is ast.InfixExpr {
732 return false
733 }
734 where_expr := node.where_expr as ast.InfixExpr
735 if where_expr.op != .eq || where_expr.left !is ast.Ident
736 || where_expr.right !is ast.SelectorExpr {
737 return false
738 }
739 key_ident := where_expr.left as ast.Ident
740 key_selector := where_expr.right as ast.SelectorExpr
741 if key_selector.expr !is ast.Ident {
742 return false
743 }
744 array_ident := key_selector.expr as ast.Ident
745 array_name := array_ident.name
746 array_elem_type := c.orm_array_object_elem_type(node, array_name) or { return false }
747 if array_elem_type.is_ptr() {
748 node.is_array_update = true
749 c.orm_error('bulk update currently supports only arrays of `${table_sym.name}` values',
750 array_ident.pos)
751 return true
752 }
753 if c.unwrap_generic(array_elem_type) != node.table_expr.typ {
754 return false
755 }
756 info := table_sym.info as ast.Struct
757 key_field := c.orm_struct_field(info.fields, key_ident.name) or {
758 c.orm_error('type `${table_sym.name}` has no field named `${key_ident.name}`',
759 key_ident.pos)
760 return true
761 }
762 selector_key_field := c.orm_struct_field(info.fields, key_selector.field_name) or {
763 c.orm_error('type `${table_sym.name}` has no field named `${key_selector.field_name}`',
764 key_selector.pos)
765 return true
766 }
767 if !c.check_types(selector_key_field.typ, key_field.typ) {
768 c.orm_error('cannot use `${key_selector.field_name}` as update key `${key_ident.name}`',
769 key_selector.pos)
770 return true
771 }
772 for i, expr in node.update_exprs {
773 if expr !is ast.SelectorExpr {
774 return false
775 }
776 selector := expr as ast.SelectorExpr
777 if selector.expr !is ast.Ident {
778 return false
779 }
780 update_array_ident := selector.expr as ast.Ident
781 if update_array_ident.name != array_name {
782 return false
783 }
784 value_field := c.orm_struct_field(info.fields, selector.field_name) or {
785 c.orm_error('type `${table_sym.name}` has no field named `${selector.field_name}`',
786 selector.pos)
787 return true
788 }
789 column := node.updated_columns[i]
790 target_field := c.get_orm_field_by_column_name(node.fields, column) or { return false }
791 target_sym := c.table.final_sym(target_field.typ.clear_flag(.option))
792 if target_sym.kind == .struct && target_sym.name != 'time.Time' {
793 c.orm_error('bulk update currently supports only primitive, enum, and time.Time fields',
794 selector.pos)
795 return true
796 }
797 if !c.check_types(value_field.typ, target_field.typ) {
798 c.orm_error('cannot use `${selector.field_name}` as update value for `${target_field.name}`',
799 selector.pos)
800 return true
801 }
802 }
803 node.is_array_update = true
804 node.array_update_var = array_name
805 node.array_update_key = c.fetch_field_name(key_field)
806 return true
807}
808
809fn (mut c Checker) orm_array_object_elem_type(node ast.SqlStmtLine, name string) ?ast.Type {
810 obj := node.scope.find(name) or { return none }
811 mut typ := obj.typ
812 if typ.is_ptr() {
813 typ = typ.deref()
814 }
815 typ = c.unwrap_generic(typ)
816 sym := c.table.sym(typ)
817 if sym.kind != .array {
818 return none
819 }
820 mut elem_type := sym.array_info().elem_type
821 return elem_type
822}
823
824fn (_ &Checker) orm_struct_field(fields []ast.StructField, name string) ?ast.StructField {
825 for field in fields {
826 if field.name == name {
827 return field
828 }
829 }
830 return none
831}
832
833fn (mut c Checker) check_orm_struct_field_attrs(node ast.SqlStmtLine, field ast.StructField) {
834 for attr in field.attrs {
835 if attr.name == 'nonull' {
836 c.warn('`nonull` attribute is deprecated; non-optional fields are always "NOT NULL", use Option fields where they can be NULL',
837 node.pos)
838 }
839 }
840}
841
842fn (mut c Checker) check_orm_non_primitive_struct_field_attrs(field ast.StructField) {
843 field_type := c.table.sym(field.typ.clear_flag(.option))
844 mut has_fkey_attr := false
845
846 for attr in field.attrs {
847 if attr.name == 'fkey' {
848 if field_type.kind != .array && field_type.kind != .struct {
849 c.orm_error('the `fkey` attribute must be used only with arrays and structures',
850 attr.pos)
851 return
852 }
853
854 if !attr.has_arg {
855 c.orm_error('the `fkey` attribute must have an argument', attr.pos)
856 return
857 }
858
859 field_struct_type := if field_type.info is ast.Array {
860 c.table.sym(field_type.info.elem_type)
861 } else {
862 field_type
863 }
864
865 field_struct_type.find_field(attr.arg) or {
866 c.orm_error('`${field_struct_type.name}` struct has no field with name `${attr.arg}`',
867 attr.pos)
868 return
869 }
870
871 has_fkey_attr = true
872 }
873 }
874
875 if field_type.kind == .array && !has_fkey_attr {
876 c.orm_error('a field that holds an array must be defined with the `fkey` attribute',
877 field.pos)
878 }
879}
880
881fn (mut c Checker) fetch_and_check_orm_fields(info ast.Struct, pos token.Pos, table_name string) []ast.StructField {
882 if cache := c.orm_table_fields[table_name] {
883 return cache
884 }
885 // Build generic names list from the struct's generic_types
886 generic_names := info.generic_types.map(c.table.sym(it).name)
887 mut fields := []ast.StructField{}
888 for field in info.fields {
889 if field.attrs.contains('skip') || field.attrs.contains_arg('sql', '-') {
890 continue
891 }
892 // Skip embedded fields, as their fields are already flattened into the struct
893 if field.is_embed {
894 embed_sym := c.table.sym(field.typ)
895 if embed_sym.info is ast.Struct {
896 // fields << c.fetch_and_check_orm_fields(embed_sym.info, pos, embed_sym.name)
897 embedded_fields := c.fetch_and_check_orm_fields(embed_sym.info, pos, embed_sym.name)
898 for ef in embedded_fields {
899 // Ensure the embedded field type is valid (not 0/unresolved)
900 if ef.typ == 0 {
901 c.orm_error('embedded struct `${embed_sym.name}` has unresolved field type for `${ef.name}`',
902 pos)
903 continue
904 }
905 mut new_field := ef
906 // Update name for correct C generation (e.g. msg.Payload.field)
907 new_field.name = '${field.name}.${ef.name}'
908 // Clone attributes to avoid modifying the original struct definition
909 new_field.attrs = ef.attrs.clone()
910
911 // Ensure SQL column name matches the original field name
912 mut has_sql := false
913 for attr in new_field.attrs {
914 if attr.name == 'sql' {
915 has_sql = true
916 break
917 }
918 }
919 if !has_sql {
920 new_field.attrs << ast.Attr{
921 name: 'sql'
922 arg: ef.name
923 has_arg: true
924 kind: .string
925 }
926 }
927 fields << new_field
928 }
929 }
930 continue
931 }
932 mut field_typ := field.typ
933 // Resolve generic field types using the struct's concrete_types if available
934 if field_typ.has_flag(.generic) && info.generic_types.len > 0
935 && info.generic_types.len == info.concrete_types.len {
936 if resolved_typ := c.table.convert_generic_type(field_typ, generic_names,
937 info.concrete_types)
938 {
939 field_typ = resolved_typ
940 }
941 }
942 // Validate field type is resolved (not 0 and not still generic)
943 if field_typ == 0 || field_typ.has_flag(.generic) {
944 c.orm_error('field `${field.name}` has unresolved type in generic struct `${table_name}` - use a concrete type instantiation',
945 field.pos)
946 continue
947 }
948 if c.orm_field_uses_anon_struct(field_typ) {
949 c.orm_error('field `${field.name}` uses an anonymous struct type, which ORM does not support; use a named struct, or skip it with `@[skip]` or `@[sql: \'-\']`',
950 field.pos)
951 continue
952 }
953 field_sym := c.table.sym(field_typ)
954 final_field_typ := c.table.final_type(field_typ)
955 is_primitive := final_field_typ.is_string() || final_field_typ.is_bool()
956 || final_field_typ.is_number()
957 is_struct := field_sym.kind == .struct
958 is_array := field_sym.kind == .array
959 is_enum := field_sym.kind == .enum
960 mut is_array_of_structs := false
961 if is_array {
962 array_info := field_sym.array_info()
963 elem_sym := c.table.sym(array_info.elem_type)
964 is_array_of_structs = elem_sym.kind == .struct
965
966 if attr := field.attrs.find_first('fkey') {
967 if attr.arg == '' {
968 c.orm_error('fkey attribute must have an argument', attr.pos)
969 }
970 } else {
971 c.orm_error('array fields must have an fkey attribute', field.pos)
972 }
973 if array_info.nr_dims > 1 || elem_sym.kind == .array {
974 c.orm_error('multi-dimension array fields are not supported', field.pos)
975 }
976 }
977 if attr := field.attrs.find_first('sql') {
978 if attr.arg == '' {
979 c.orm_error('sql attribute must have an argument', attr.pos)
980 }
981 }
982 if is_primitive || is_struct || is_enum || is_array_of_structs {
983 // Use the resolved type in the field
984 mut resolved_field := field
985 resolved_field.typ = field_typ
986 fields << resolved_field
987 }
988 }
989 if fields.len == 0 {
990 c.orm_error('select: empty fields in `${table_name}`', pos)
991 }
992 if attr := info.attrs.find_first('table') {
993 if attr.arg == '' {
994 c.orm_error('table attribute must have an argument', attr.pos)
995 }
996 }
997 c.orm_table_fields[table_name] = fields
998 return fields
999}
1000
1001fn (c &Checker) orm_field_uses_anon_struct(field_typ ast.Type) bool {
1002 final_field_typ := c.table.final_type(field_typ.clear_flag(.option))
1003 field_sym := c.table.sym(final_field_typ)
1004 if field_sym.kind == .struct && field_sym.info is ast.Struct && field_sym.info.is_anon {
1005 return true
1006 }
1007 if field_sym.kind != .array {
1008 return false
1009 }
1010 array_info := field_sym.array_info()
1011 elem_typ := c.table.final_type(array_info.elem_type.clear_flag(.option))
1012 elem_sym := c.table.sym(elem_typ)
1013 return elem_sym.kind == .struct && elem_sym.info is ast.Struct && elem_sym.info.is_anon
1014}
1015
1016// check_sql_value_expr_is_comptime_with_natural_number_or_expr_with_int_type checks that an expression is compile-time
1017// and contains an integer greater than or equal to zero or it is a runtime expression with an integer type.
1018fn (mut c Checker) check_sql_value_expr_is_comptime_with_natural_number_or_expr_with_int_type(mut expr ast.Expr,
1019 sql_keyword string) {
1020 comptime_number := c.get_comptime_number_value(mut expr) or {
1021 c.check_sql_expr_type_is_int(expr, sql_keyword)
1022 return
1023 }
1024
1025 if comptime_number < 0 {
1026 c.orm_error('`${sql_keyword}` must be greater than or equal to zero', expr.pos())
1027 }
1028}
1029
1030fn (mut c Checker) check_orm_aggregate_field(kind ast.SqlAggregateKind, field_name string,
1031 fields []ast.StructField, table_name string, pos token.Pos) ?ast.StructField {
1032 field := fields.filter(it.name == field_name)
1033 if field.len == 0 {
1034 mut field_names := []string{cap: fields.len}
1035 for item in fields {
1036 field_names << item.name
1037 }
1038 c.orm_error(util.new_suggestion(field_name, field_names).say('`${table_name}` structure has no field with name `${field_name}`'),
1039 pos)
1040 return none
1041 }
1042 resolved_field := field[0]
1043 field_type := c.table.final_type(resolved_field.typ.clear_flag(.option))
1044 field_sym := c.table.sym(field_type)
1045 is_time := field_sym.name == 'time.Time'
1046 is_numeric := field_type.is_number()
1047 is_string := field_type.is_string()
1048
1049 if field_sym.kind in [.array, .struct] && !is_time {
1050 c.orm_error('ORM aggregate functions do not support array or sub-struct fields', pos)
1051 return none
1052 }
1053
1054 match kind {
1055 .sum, .avg {
1056 if !is_numeric {
1057 msg := match kind {
1058 .sum { '`sum` aggregate requires a numeric field' }
1059 .avg { '`avg` aggregate requires a numeric field' }
1060 else { 'aggregate requires a numeric field' }
1061 }
1062
1063 c.orm_error(msg, pos)
1064 return none
1065 }
1066 }
1067 .min, .max {
1068 if !(is_numeric || is_string || is_time) {
1069 msg := match kind {
1070 .min { '`min` aggregate requires a numeric, string, or time.Time field' }
1071 .max { '`max` aggregate requires a numeric, string, or time.Time field' }
1072 else { 'aggregate requires a numeric, string, or time.Time field' }
1073 }
1074
1075 c.orm_error(msg, pos)
1076 return none
1077 }
1078 }
1079 else {}
1080 }
1081
1082 return resolved_field
1083}
1084
1085fn (_ &Checker) orm_aggregate_return_type(kind ast.SqlAggregateKind, field_type ast.Type) ast.Type {
1086 return match kind {
1087 .count { ast.int_type }
1088 .avg { ast.f64_type.set_flag(.option) }
1089 .sum, .min, .max { field_type.clear_flag(.option).set_flag(.option) }
1090 .none { ast.void_type }
1091 }
1092}
1093
1094fn (mut c Checker) check_sql_expr_type_is_int(expr &ast.Expr, sql_keyword string) {
1095 if expr is ast.Ident {
1096 if expr.obj.typ.is_int() {
1097 return
1098 }
1099 } else if expr is ast.SelectorExpr {
1100 if expr.typ.is_int() {
1101 return
1102 }
1103 } else if expr is ast.CallExpr {
1104 if expr.return_type == 0 {
1105 return
1106 }
1107
1108 type_symbol := c.table.sym(expr.return_type)
1109 is_error_type := expr.return_type.has_flag(.result) || expr.return_type.has_flag(.option)
1110 is_acceptable_type := type_symbol.is_int() && !is_error_type
1111
1112 if !is_acceptable_type {
1113 error_type_symbol := c.fn_return_type_flag_to_string(expr.return_type)
1114 c.orm_error('function calls in `${sql_keyword}` must return only an integer type, but `${expr.name}` returns `${error_type_symbol}${type_symbol.name}`',
1115 expr.pos)
1116 }
1117
1118 return
1119 } else if expr is ast.ParExpr {
1120 c.check_sql_expr_type_is_int(expr.expr, sql_keyword)
1121 return
1122 }
1123
1124 c.orm_error('the type of `${sql_keyword}` must be an integer type', expr.pos())
1125}
1126
1127fn (mut c Checker) orm_error(message string, pos token.Pos) {
1128 c.error('ORM: ${message}', pos)
1129}
1130
1131// check_expr_has_no_fn_calls_with_non_orm_return_type checks that an expression has no function calls
1132// that return complex types which can't be transformed into SQL.
1133fn (mut c Checker) check_expr_has_no_fn_calls_with_non_orm_return_type(expr &ast.Expr) {
1134 if expr is ast.CallExpr {
1135 // `expr.return_type` may be empty. For example, a user call function incorrectly without passing all required arguments.
1136 // This error will be handled in another place. Otherwise, `c.table.sym` below does panic.
1137 //
1138 // fn test(flag bool) {}
1139 // test()
1140 // ~~~~~~ expected 1 arguments, but got 0
1141 if expr.return_type == 0 {
1142 return
1143 }
1144
1145 type_symbol := c.table.sym(expr.return_type)
1146 is_time := type_symbol.cname == 'time__Time'
1147 is_not_pointer := !type_symbol.is_pointer()
1148 is_error_type := expr.return_type.has_flag(.result) || expr.return_type.has_flag(.option)
1149 is_acceptable_type := (type_symbol.is_primitive() || is_time) && is_not_pointer
1150 && !is_error_type
1151
1152 if !is_acceptable_type {
1153 error_type_symbol := c.fn_return_type_flag_to_string(expr.return_type)
1154 c.orm_error('function calls must return only primitive types and time.Time, but `${expr.name}` returns `${error_type_symbol}${type_symbol.name}`',
1155 expr.pos)
1156 }
1157 } else if expr is ast.ParExpr {
1158 c.check_expr_has_no_fn_calls_with_non_orm_return_type(expr.expr)
1159 } else if expr is ast.InfixExpr {
1160 c.check_expr_has_no_fn_calls_with_non_orm_return_type(expr.left)
1161 c.check_expr_has_no_fn_calls_with_non_orm_return_type(expr.right)
1162 if expr.right_type.has_flag(.option) && expr.op !in [.key_is, .not_is] {
1163 c.warn('comparison with Option value probably isn\'t intended; use "is none" and "!is none" to select by NULL',
1164 expr.pos)
1165 } else if expr.right_type == ast.none_type && expr.op !in [.key_is, .not_is] {
1166 c.warn('comparison with none probably isn\'t intended; use "is none" and "!is none" to select by NULL',
1167 expr.pos)
1168 }
1169 }
1170}
1171
1172// check_where_data_expr_has_no_struct_field_refs checks that expressions destined for ORM bind data
1173// do not reference fields from the queried table, because ORM where clauses currently only support
1174// comparing a table field with a V value, not with another table field.
1175fn (mut c Checker) check_where_data_expr_has_no_struct_field_refs(table_type_symbol &ast.TypeSymbol, expr ast.Expr, op token.Kind) {
1176 match expr {
1177 ast.Ident {
1178 if expr.kind == .unresolved && table_type_symbol.has_field(expr.name) {
1179 c.orm_error('right side of the `${op}` expression cannot reference another `${table_type_symbol.name}` field; field-to-field comparisons are not supported',
1180 expr.pos)
1181 }
1182 }
1183 ast.ArrayInit {
1184 for item in expr.exprs {
1185 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, item, op)
1186 }
1187 }
1188 ast.CallExpr {
1189 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.left, op)
1190 for arg in expr.args {
1191 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, arg.expr, op)
1192 }
1193 }
1194 ast.CastExpr {
1195 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.expr, op)
1196 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.arg, op)
1197 }
1198 ast.IndexExpr {
1199 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.left, op)
1200 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.index, op)
1201 }
1202 ast.InfixExpr {
1203 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.left, op)
1204 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.right, op)
1205 }
1206 ast.MapInit {
1207 for key in expr.keys {
1208 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, key, op)
1209 }
1210 for val in expr.vals {
1211 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, val, op)
1212 }
1213 }
1214 ast.ParExpr {
1215 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.expr, op)
1216 }
1217 ast.PrefixExpr {
1218 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.right, op)
1219 }
1220 ast.SelectorExpr {
1221 table_name := util.strip_mod_name(table_type_symbol.name)
1222 if table_type_symbol.has_field(expr.field_name) {
1223 if expr.expr is ast.TypeNode
1224 && c.table.sym(expr.expr.typ).name == table_type_symbol.name {
1225 c.orm_error('right side of the `${op}` expression cannot reference another `${table_type_symbol.name}` field; field-to-field comparisons are not supported',
1226 expr.pos)
1227 } else if expr.expr is ast.Ident && expr.expr.kind == .unresolved
1228 && expr.expr.name == table_name {
1229 c.orm_error('right side of the `${op}` expression cannot reference another `${table_type_symbol.name}` field; field-to-field comparisons are not supported',
1230 expr.pos)
1231 }
1232 }
1233 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.expr, op)
1234 }
1235 ast.StringInterLiteral {
1236 for interpolated in expr.exprs {
1237 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, interpolated,
1238 op)
1239 }
1240 }
1241 ast.StructInit {
1242 for init_field in expr.init_fields {
1243 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol,
1244 init_field.expr, op)
1245 }
1246 }
1247 ast.UnsafeExpr {
1248 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.expr, op)
1249 }
1250 else {}
1251 }
1252}
1253
1254// check_where_expr_has_no_pointless_exprs checks that an expression has no pointless expressions
1255// which don't affect the result. For example, `where 3` is pointless.
1256// Also, it checks that the left side of the infix expression is always the structure field.
1257fn (mut c Checker) check_where_expr_has_no_pointless_exprs(table_type_symbol &ast.TypeSymbol, field_names []string,
1258 expr &ast.Expr) {
1259 // Skip type checking for generated subqueries
1260 // that are not linked to scope and vars but only created for cgen.
1261 if expr is ast.None {
1262 return
1263 }
1264
1265 if expr is ast.InfixExpr {
1266 has_no_field_error := "left side of the `${expr.op}` expression must be one of the `${table_type_symbol.name}`'s fields"
1267
1268 if expr.left is ast.Ident {
1269 left_ident_name := expr.left.name
1270
1271 if !table_type_symbol.has_field(left_ident_name) {
1272 c.orm_error(util.new_suggestion(left_ident_name, field_names).say(has_no_field_error),
1273 expr.left.pos)
1274 }
1275 } else if expr.left is ast.InfixExpr || expr.left is ast.ParExpr
1276 || expr.left is ast.PrefixExpr {
1277 c.check_where_expr_has_no_pointless_exprs(table_type_symbol, field_names, expr.left)
1278 } else if !(expr.left is ast.SelectorExpr
1279 && c.comptime.is_comptime_selector_field_name(expr.left, 'name')) {
1280 c.orm_error(has_no_field_error, expr.left.pos())
1281 }
1282
1283 if expr.op in [.ne, .eq, .lt, .gt, .ge, .le, .key_like, .key_ilike, .key_in, .not_in] {
1284 c.check_where_data_expr_has_no_struct_field_refs(table_type_symbol, expr.right, expr.op)
1285 } else if expr.right is ast.InfixExpr || expr.right is ast.ParExpr
1286 || expr.right is ast.PrefixExpr {
1287 c.check_where_expr_has_no_pointless_exprs(table_type_symbol, field_names, expr.right)
1288 }
1289 } else if expr is ast.ParExpr {
1290 c.check_where_expr_has_no_pointless_exprs(table_type_symbol, field_names, expr.expr)
1291 } else if expr is ast.PrefixExpr {
1292 c.check_where_expr_has_no_pointless_exprs(table_type_symbol, field_names, expr.right)
1293 } else {
1294 c.orm_error('`where` expression must have at least one comparison for filtering rows',
1295 expr.pos())
1296 }
1297}
1298
1299fn (_ &Checker) fn_return_type_flag_to_string(typ ast.Type) string {
1300 is_result_type := typ.has_flag(.result)
1301 is_option_type := typ.has_flag(.option)
1302
1303 return if is_result_type {
1304 '!'
1305 } else if is_option_type {
1306 '?'
1307 } else {
1308 ''
1309 }
1310}
1311
1312fn (mut c Checker) check_orm_or_expr(mut expr ORMExpr) {
1313 if mut expr is ast.SqlExpr {
1314 if expr.is_generated {
1315 return
1316 }
1317 }
1318 return_type := if mut expr is ast.SqlExpr {
1319 expr.typ
1320 } else {
1321 ast.void_type.set_flag(.result)
1322 }
1323
1324 if expr.or_expr.kind == .absent {
1325 if c.inside_defer {
1326 c.error('ORM returns a result, so it should have an `or {}` block at the end', expr.pos)
1327 } else {
1328 c.error('ORM returns a result, so it should have either an `or {}` block, or `!` at the end',
1329 expr.pos)
1330 }
1331 } else {
1332 c.check_or_expr(expr.or_expr, return_type.clear_flag(.result), return_type, if mut expr is ast.SqlExpr {
1333 expr
1334 } else {
1335 ast.empty_expr
1336 })
1337 }
1338
1339 if expr.or_expr.kind == .block {
1340 c.expected_or_type = return_type.clear_flag(.result)
1341 c.stmts_ending_with_expression(mut expr.or_expr.stmts, c.expected_or_type)
1342 c.expected_or_type = ast.void_type
1343 }
1344}
1345
1346// check_db_expr checks the `db_expr` implements `orm.Connection` and has no `option` flag.
1347fn (mut c Checker) check_db_expr(mut db_expr ast.Expr) bool {
1348 connection_type_index := c.table.find_type('orm.Connection')
1349 connection_typ := connection_type_index
1350 db_expr_type := c.expr(mut db_expr)
1351
1352 // If we didn't find `orm.Connection`, we don't have any imported modules
1353 // that depend on `orm` and implement the `orm.Connection` interface.
1354 if connection_type_index == 0 {
1355 c.error('expected a type that implements the `orm.Connection` interface', db_expr.pos())
1356 return false
1357 }
1358
1359 is_implemented := c.type_implements(db_expr_type, connection_typ, db_expr.pos())
1360 is_option := db_expr_type.has_flag(.option)
1361
1362 if is_implemented && is_option {
1363 c.error(c.expected_msg(db_expr_type, db_expr_type.clear_flag(.option)), db_expr.pos())
1364 return false
1365 }
1366
1367 return true
1368}
1369
1370fn (mut c Checker) resolve_orm_table_expr_type(mut type_node ast.TypeNode) {
1371 resolved_typ := c.unwrap_generic(type_node.typ)
1372 if resolved_typ != type_node.typ {
1373 type_node.typ = resolved_typ
1374 }
1375}
1376
1377fn (mut c Checker) check_orm_table_expr_type(type_node &ast.TypeNode) bool {
1378 table_sym := c.table.sym(type_node.typ)
1379
1380 if table_sym.info !is ast.Struct {
1381 c.orm_error('the table symbol `${table_sym.name}` has to be a struct', type_node.pos)
1382
1383 return false
1384 }
1385
1386 return true
1387}
1388
1389// get_field_foreign_table_type gets the type of table in which the primary key
1390// is referred to by the provided field. For example, the `[]Child` field
1391// refers to the foreign table `Child`.
1392fn (c &Checker) get_field_foreign_table_type(table_field &ast.StructField) ast.Type {
1393 field_type := table_field.typ.clear_flag(.option)
1394 field_sym := c.table.sym(field_type)
1395 if field_sym.kind == .struct {
1396 return field_type
1397 } else if field_sym.kind == .array {
1398 return field_sym.array_info().elem_type
1399 } else {
1400 return ast.no_type
1401 }
1402}
1403
1404// get_orm_non_primitive_fields filters the table fields by selecting only
1405// non-primitive fields such as arrays and structs.
1406fn (c &Checker) get_orm_non_primitive_fields(fields []ast.StructField) []ast.StructField {
1407 mut res := []ast.StructField{}
1408 for field in fields {
1409 type_with_no_option_flag := field.typ.clear_flag(.option)
1410 field_sym := c.table.sym(type_with_no_option_flag)
1411 is_struct := field_sym.kind == .struct
1412 is_array := field_sym.kind == .array
1413 is_array_with_struct_elements := is_array
1414 && c.table.sym(field_sym.array_info().elem_type).kind == .struct
1415 is_time := c.table.get_type_name(type_with_no_option_flag) == 'time.Time'
1416
1417 if (is_struct || is_array_with_struct_elements) && !is_time {
1418 res << field
1419 }
1420 }
1421 return res
1422}
1423
1424fn (mut c Checker) get_orm_field_by_column_name(fields []ast.StructField, column string) ?ast.StructField {
1425 for field in fields {
1426 if c.fetch_field_name(field) == column {
1427 return field
1428 }
1429 }
1430 return none
1431}
1432
1433fn (mut c Checker) resolve_orm_selected_fields(requested_fields []ast.SqlSelectField, fields []ast.StructField, table_sym &ast.TypeSymbol) ?[]ast.StructField {
1434 field_names := fields.map(it.name)
1435 short_table_name := util.strip_mod_name(table_sym.name)
1436 mut selected_field_names := map[string]bool{}
1437 for requested_field in requested_fields {
1438 mut field_name := requested_field.name
1439 if field_name.starts_with('${table_sym.name}.') {
1440 field_name = field_name.all_after('${table_sym.name}.')
1441 } else if field_name.starts_with('${short_table_name}.') {
1442 field_name = field_name.all_after('${short_table_name}.')
1443 }
1444 if field_name !in field_names {
1445 c.orm_error(util.new_suggestion(field_name, field_names).say('`${table_sym.name}` structure has no field with name `${field_name}`'),
1446 requested_field.pos)
1447 return none
1448 }
1449 selected_field_names[field_name] = true
1450 }
1451 mut selected_fields := []ast.StructField{cap: requested_fields.len}
1452 for field in fields {
1453 if field.name in selected_field_names {
1454 selected_fields << field
1455 }
1456 }
1457 return selected_fields
1458}
1459
1460// walkingdevel: Now I don't think it's a good solution
1461// because it only checks structure initialization,
1462// but structure fields may be updated later before inserting.
1463// For example,
1464// ```v
1465// mut package := Package{
1466// name: 'xml'
1467// }
1468//
1469// package.author = User{
1470// username: 'walkingdevel'
1471// }
1472// ```
1473// TODO: rewrite it, move to runtime.
1474fn (_ &Checker) check_field_of_inserting_struct_is_uninitialized(node &ast.SqlStmtLine, field_name string) bool {
1475 struct_scope := node.scope.find_var(node.object_var) or { return false }
1476
1477 if struct_scope.expr is ast.StructInit {
1478 return struct_scope.expr.init_fields.filter(it.name == field_name).len == 0
1479 }
1480
1481 return false
1482}
1483
1484// check_recursive_structs returns true if type is struct and has any child or nested child with the type of the given struct name,
1485// array elements are all checked.
1486fn (mut c Checker) check_recursive_structs(ts &ast.TypeSymbol, struct_name string) bool {
1487 if ts.info is ast.Struct {
1488 for field in ts.info.fields {
1489 _, field_sym := c.get_non_array_type(field.typ)
1490 if field_sym.kind == .struct && field_sym.name == struct_name {
1491 return true
1492 }
1493 }
1494 }
1495 return false
1496}
1497
1498// returns the final non-array type by recursively retrieving the element type of an array type,
1499// if the input type is not an array, it is returned directly.
1500fn (mut c Checker) get_non_array_type(typ_ ast.Type) (ast.Type, &ast.TypeSymbol) {
1501 mut typ := typ_
1502 mut sym := c.table.sym(typ)
1503 for {
1504 if sym.kind == .array {
1505 typ = sym.array_info().elem_type
1506 sym = c.table.sym(typ)
1507 } else {
1508 break
1509 }
1510 }
1511 return typ, sym
1512}
1513
1514// check_orm_join_clause validates a JOIN clause in an ORM query.
1515// It checks that the joined table type exists and is a struct,
1516// and validates the ON expression.
1517fn (mut c Checker) check_orm_join_clause(mut join ast.JoinClause, main_table_sym &ast.TypeSymbol) bool {
1518 c.resolve_orm_table_expr_type(mut join.table_expr)
1519
1520 // Check that the joined table type exists
1521 if !c.ensure_type_exists(join.table_expr.typ, join.pos) {
1522 return false
1523 }
1524
1525 join_table_sym := c.table.sym(join.table_expr.typ)
1526
1527 // Check that the joined table is a struct
1528 if join_table_sym.info !is ast.Struct {
1529 c.orm_error('JOIN table `${join_table_sym.name}` must be a struct type', join.pos)
1530 return false
1531 }
1532
1533 // Validate the ON expression structure without running full expression checking
1534 // (since Table.field syntax is special in ORM context)
1535 return c.check_orm_join_on_expr(join.on_expr, main_table_sym, join_table_sym)
1536}
1537
1538// check_orm_join_on_expr validates the ON expression of a JOIN clause.
1539// It expects the form: TableA.fieldA == TableB.fieldB
1540// where TableA is either the main table or the joined table.
1541fn (mut c Checker) check_orm_join_on_expr(on_expr ast.Expr, main_table_sym &ast.TypeSymbol, join_table_sym &ast.TypeSymbol) bool {
1542 // The ON expression should be an infix expression (e.g., Table1.field == Table2.field)
1543 if on_expr is ast.InfixExpr {
1544 // Check that the operator is a comparison operator
1545 if on_expr.op !in [.eq, .ne, .lt, .gt, .le, .ge] {
1546 c.orm_error('JOIN ON condition must use a comparison operator (==, !=, <, >, <=, >=)',
1547 on_expr.pos)
1548 return false
1549 }
1550
1551 // Check left side (should be Table.field format)
1552 if !c.check_orm_join_field_ref(on_expr.left, main_table_sym, join_table_sym) {
1553 return false
1554 }
1555
1556 // Check right side (should be Table.field format)
1557 if !c.check_orm_join_field_ref(on_expr.right, main_table_sym, join_table_sym) {
1558 return false
1559 }
1560
1561 return true
1562 } else if on_expr !is ast.EmptyExpr {
1563 c.orm_error('JOIN ON condition must be a comparison expression (e.g., Table1.field == Table2.field)',
1564 on_expr.pos())
1565 return false
1566 }
1567
1568 return true
1569}
1570
1571// check_orm_join_field_ref validates that an expression is a valid Table.field reference
1572// for a JOIN ON condition. The table must be either the main table or the joined table.
1573fn (mut c Checker) check_orm_join_field_ref(expr ast.Expr, main_table_sym &ast.TypeSymbol, join_table_sym &ast.TypeSymbol) bool {
1574 // Handle SelectorExpr (e.g., User.department_id)
1575 if expr is ast.SelectorExpr {
1576 // Get the table name from the selector's left side
1577 mut table_name := ''
1578 if expr.expr is ast.Ident {
1579 table_name = expr.expr.name
1580 } else if expr.expr is ast.TypeNode {
1581 table_name = c.table.sym(expr.expr.typ).name
1582 }
1583
1584 // Check if the table name matches either the main table or the joined table
1585 main_table_name := util.strip_mod_name(main_table_sym.name)
1586 join_table_name := util.strip_mod_name(join_table_sym.name)
1587
1588 // Determine which table to check the field against
1589 is_main_table := table_name == main_table_name
1590 is_join_table := table_name == join_table_name
1591
1592 if !is_main_table && !is_join_table {
1593 c.orm_error('table `${table_name}` in JOIN ON condition must be either `${main_table_name}` or `${join_table_name}`',
1594 expr.pos)
1595 return false
1596 }
1597
1598 // Check if the field exists in the target table
1599 field_name := expr.field_name
1600 if is_main_table {
1601 if !main_table_sym.has_field(field_name) {
1602 c.orm_error('field `${field_name}` does not exist in table `${main_table_name}`',
1603 expr.pos)
1604 return false
1605 }
1606 } else {
1607 if !join_table_sym.has_field(field_name) {
1608 c.orm_error('field `${field_name}` does not exist in table `${join_table_name}`',
1609 expr.pos)
1610 return false
1611 }
1612 }
1613
1614 return true
1615 }
1616
1617 // Handle EnumVal - this happens when the parser sees Type.field as an enum access
1618 // In ORM context, we need to reinterpret this as a table field reference
1619 if expr is ast.EnumVal {
1620 // The enum_name is the table name (e.g., "User")
1621 // The val is the field name (e.g., "department_id")
1622 table_name := util.strip_mod_name(expr.enum_name)
1623 field_name := expr.val
1624
1625 main_table_name := util.strip_mod_name(main_table_sym.name)
1626 join_table_name := util.strip_mod_name(join_table_sym.name)
1627
1628 is_main_table := table_name == main_table_name
1629 is_join_table := table_name == join_table_name
1630
1631 if !is_main_table && !is_join_table {
1632 c.orm_error('table `${table_name}` in JOIN ON condition must be either `${main_table_name}` or `${join_table_name}`',
1633 expr.pos)
1634 return false
1635 }
1636
1637 // Check if the field exists in the target table
1638 if is_main_table {
1639 if !main_table_sym.has_field(field_name) {
1640 c.orm_error('field `${field_name}` does not exist in table `${main_table_name}`',
1641 expr.pos)
1642 return false
1643 }
1644 } else {
1645 if !join_table_sym.has_field(field_name) {
1646 c.orm_error('field `${field_name}` does not exist in table `${join_table_name}`',
1647 expr.pos)
1648 return false
1649 }
1650 }
1651
1652 return true
1653 }
1654
1655 c.orm_error('JOIN ON condition expects Table.field format (got ${typeof(expr).name})',
1656 expr.pos())
1657 return false
1658}
1659