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