v2 / vlib / orm / orm.v
1624 lines · 1479 sloc · 38.46 KB · d5578efae67ef01ab3a63866d429b7dacf6c237d
Raw
1@[has_globals]
2module orm
3
4import time
5
6const default_tenant_filter_field_name = 'tenant_id'
7const tenant_filter_attr_name = 'tenant_filter'
8const tenant_field_attr_name = 'tenant_field'
9const ignore_tenant_filter_attr_name = 'ignore_tenant_filter'
10
11pub const num64 = [typeof[i64]().idx, typeof[u64]().idx]
12pub const nums = [
13 typeof[i8]().idx,
14 typeof[i16]().idx,
15 typeof[int]().idx,
16 typeof[u8]().idx,
17 typeof[u16]().idx,
18 typeof[u32]().idx,
19 typeof[bool]().idx,
20]
21pub const float = [
22 typeof[f32]().idx,
23 typeof[f64]().idx,
24]
25pub const type_string = typeof[string]().idx
26pub const serial = -1
27pub const time_ = -2
28pub const enum_ = -3
29pub const type_idx = {
30 'i8': typeof[i8]().idx
31 'i16': typeof[i16]().idx
32 'int': typeof[int]().idx
33 'i64': typeof[i64]().idx
34 'u8': typeof[u8]().idx
35 'u16': typeof[u16]().idx
36 'u32': typeof[u32]().idx
37 'u64': typeof[u64]().idx
38 'f32': typeof[f32]().idx
39 'f64': typeof[f64]().idx
40 'bool': typeof[bool]().idx
41 'string': typeof[string]().idx
42}
43pub const string_max_len = 2048
44pub const null_primitive = Primitive(Null{})
45
46pub type Primitive = Null
47 | bool
48 | f32
49 | f64
50 | i16
51 | i64
52 | i8
53 | int
54 | string
55 | time.Time
56 | u16
57 | u32
58 | u64
59 | u8
60 | InfixType
61 | []bool
62 | []f32
63 | []f64
64 | []i16
65 | []i64
66 | []i8
67 | []int
68 | []string
69 | []time.Time
70 | []u16
71 | []u32
72 | []u64
73 | []u8
74 | []InfixType
75 | []Primitive
76
77pub struct Null {}
78
79pub enum OperationKind {
80 neq // !=
81 eq // ==
82 gt // >
83 lt // <
84 ge // >=
85 le // <=
86 orm_like // LIKE
87 orm_ilike // ILIKE
88 is_null // IS NULL
89 is_not_null // IS NOT NULL
90 in // IN
91 not_in // NOT IN
92}
93
94pub enum MathOperationKind {
95 add // +
96 sub // -
97 mul // *
98 div // /
99}
100
101pub enum StmtKind {
102 insert
103 update
104 delete
105}
106
107pub enum OrderType {
108 asc
109 desc
110}
111
112pub enum AggregateKind {
113 none
114 count
115 sum
116 avg
117 min
118 max
119}
120
121// JoinType represents the type of SQL JOIN operation
122pub enum JoinType {
123 inner // INNER JOIN - returns only matching rows
124 left // LEFT JOIN - returns all left rows, NULL for non-matching right
125 right // RIGHT JOIN - returns all right rows, NULL for non-matching left
126 full_outer // FULL OUTER JOIN - returns all rows from both tables
127}
128
129fn (jt JoinType) to_str() string {
130 return match jt {
131 .inner { 'INNER JOIN' }
132 .left { 'LEFT JOIN' }
133 .right { 'RIGHT JOIN' }
134 .full_outer { 'FULL OUTER JOIN' }
135 }
136}
137
138// JoinConfig holds configuration for a JOIN clause in a SELECT query
139pub struct JoinConfig {
140pub mut:
141 kind JoinType
142 table Table
143 on_left_col string // Column from main table (e.g., 'user_id')
144 on_right_col string // Column from joined table (e.g., 'id')
145}
146
147pub enum SQLDialect {
148 default
149 h2
150 mysql
151 pg
152 sqlite
153}
154
155fn (kind OperationKind) to_str() string {
156 str := match kind {
157 // While most SQL databases support "!=" for not equal, "<>" is the standard
158 // operator.
159 .neq { '<>' }
160 .eq { '=' }
161 .gt { '>' }
162 .lt { '<' }
163 .ge { '>=' }
164 .le { '<=' }
165 .orm_like { 'LIKE' }
166 .orm_ilike { 'ILIKE' }
167 .is_null { 'IS NULL' }
168 .is_not_null { 'IS NOT NULL' }
169 .in { 'IN' }
170 .not_in { 'NOT IN' }
171 }
172
173 return str
174}
175
176fn (kind OperationKind) is_unary() bool {
177 return kind in [.is_null, .is_not_null]
178}
179
180fn (kind OrderType) to_str() string {
181 return match kind {
182 .desc {
183 'DESC'
184 }
185 .asc {
186 'ASC'
187 }
188 }
189}
190
191fn (kind AggregateKind) to_str() string {
192 return match kind {
193 .none { '' }
194 .count { 'COUNT(*)' }
195 .sum { 'SUM' }
196 .avg { 'AVG' }
197 .min { 'MIN' }
198 .max { 'MAX' }
199 }
200}
201
202// Examples for QueryData in SQL: abc == 3 && b == 'test'
203// => fields[abc, b]; data[3, 'test']; types[index of int, index of string]; kinds[.eq, .eq]; is_and[true];
204// Every field, data, type & kind of operation in the expr share the same index in the arrays
205// is_and defines how they're addicted to each other either and or or
206// parentheses defines which fields will be inside ()
207// auto_fields are indexes of fields where db should generate a value when absent in an insert
208pub struct QueryData {
209pub mut:
210 fields []string
211 data []Primitive
212 types []int
213 parentheses [][]int
214 kinds []OperationKind
215 auto_fields []int
216 is_and []bool
217 batch_rows int
218 batch_key string
219}
220
221pub struct InfixType {
222pub:
223 name string
224 operator MathOperationKind
225 right Primitive
226}
227
228pub struct Table {
229pub mut:
230 name string
231 attrs []VAttribute
232}
233
234pub struct TableField {
235pub mut:
236 name string
237 typ int
238 nullable bool
239 default_val string
240 attrs []VAttribute
241 is_arr bool
242}
243
244// table - Table struct
245// aggregate_kind - Select rows or return a single aggregate value
246// has_where - Select all or use a where expr
247// has_order - Order the results
248// order - Name of the column which will be ordered
249// order_type - Type of order (asc, desc)
250// has_limit - Limits the output data
251// primary - Name of the primary field
252// has_offset - Add an offset to the result
253// fields - Fields to select
254// types - Types to select
255// joins - JOIN clauses for this query
256pub struct SelectConfig {
257pub mut:
258 table Table
259 aggregate_kind AggregateKind
260 aggregate_field string
261 has_where bool
262 has_order bool
263 order string
264 order_type OrderType
265 has_limit bool
266 primary string = 'id' // should be set if primary is different than 'id' and 'has_limit' is false
267 has_offset bool
268 has_distinct bool
269 fields []string
270 select_exprs []string
271 types []int
272 joins []JoinConfig // JOIN clauses for this query
273}
274
275struct TenantFilterState {
276mut:
277 enabled bool
278 field_name string
279 has_current_tenant bool
280 current_tenant Primitive
281}
282
283struct TenantFilterScopeState {
284 enabled bool
285 has_current_tenant bool
286 current_tenant Primitive
287}
288
289pub struct TenantFilterConfig {
290pub:
291 enabled bool = true
292 field_name string = default_tenant_filter_field_name
293}
294
295__global tenant_filter_state = TenantFilterState{
296 enabled: false
297 field_name: default_tenant_filter_field_name
298 has_current_tenant: false
299 current_tenant: null_primitive
300}
301
302// Interfaces gets called from the backend and can be implemented
303// Since the orm supports arrays aswell, they have to be returned too.
304// A row is represented as []Primitive, where the data is connected to the fields of the struct by their
305// index. The indices are mapped with the SelectConfig.field array. This is the mapping for a struct.
306// To have an array, there has to be an array of structs, basically [][]Primitive
307//
308// Every function without last_id() returns an optional, which returns an error if present
309// last_id returns the last inserted id of the db
310pub interface Connection {
311mut:
312 select(config SelectConfig, data QueryData, where QueryData) ![][]Primitive
313 insert(table Table, data QueryData) !
314 update(table Table, data QueryData, where QueryData) !
315 delete(table Table, where QueryData) !
316 create(table Table, fields []TableField) !
317 drop(table Table) !
318 last_id() int
319}
320
321// TransactionalConnection extends Connection with transaction primitives.
322pub interface TransactionalConnection {
323 Connection
324mut:
325 orm_begin() !
326 orm_commit() !
327 orm_rollback() !
328 orm_savepoint(name string) !
329 orm_rollback_to(name string) !
330 orm_release_savepoint(name string) !
331}
332
333// configure_tenant_filter configures the global ORM tenant filter behavior.
334pub fn configure_tenant_filter(config TenantFilterConfig) {
335 tenant_filter_state.enabled = config.enabled
336 tenant_filter_state.field_name = normalize_tenant_filter_field_name(config.field_name)
337}
338
339// set_tenant_filter_enabled enables or disables global tenant filtering.
340pub fn set_tenant_filter_enabled(enabled bool) {
341 tenant_filter_state.enabled = enabled
342}
343
344// set_current_tenant_id sets the current tenant id used by global tenant filtering.
345pub fn set_current_tenant_id(tenant_id Primitive) {
346 if tenant_id is Null {
347 clear_current_tenant_id()
348 return
349 }
350 tenant_filter_state.has_current_tenant = true
351 tenant_filter_state.current_tenant = tenant_id
352}
353
354// clear_current_tenant_id clears the current tenant id used by global tenant filtering.
355pub fn clear_current_tenant_id() {
356 tenant_filter_state.has_current_tenant = false
357 tenant_filter_state.current_tenant = null_primitive
358}
359
360// with_tenant executes `callback` with a temporary tenant id and enabled tenant filtering.
361pub fn with_tenant[T](tenant_id Primitive, callback fn () !T) !T {
362 saved := tenant_filter_scope_snapshot()
363 tenant_filter_state.enabled = true
364 tenant_filter_state.has_current_tenant = true
365 tenant_filter_state.current_tenant = tenant_id
366 defer {
367 tenant_filter_scope_restore(saved)
368 }
369 return callback()
370}
371
372// with_tenant_value executes `callback` with a temporary tenant id and enabled tenant filtering.
373pub fn with_tenant_value[T](tenant_id Primitive, callback fn () T) T {
374 saved := tenant_filter_scope_snapshot()
375 tenant_filter_state.enabled = true
376 tenant_filter_state.has_current_tenant = true
377 tenant_filter_state.current_tenant = tenant_id
378 defer {
379 tenant_filter_scope_restore(saved)
380 }
381 return callback()
382}
383
384// without_tenant_filter executes `callback` with tenant filtering temporarily disabled.
385pub fn without_tenant_filter[T](callback fn () !T) !T {
386 saved := tenant_filter_scope_snapshot()
387 tenant_filter_state.enabled = false
388 defer {
389 tenant_filter_scope_restore(saved)
390 }
391 return callback()
392}
393
394// without_tenant_filter_value executes `callback` with tenant filtering temporarily disabled.
395pub fn without_tenant_filter_value[T](callback fn () T) T {
396 saved := tenant_filter_scope_snapshot()
397 tenant_filter_state.enabled = false
398 defer {
399 tenant_filter_scope_restore(saved)
400 }
401 return callback()
402}
403
404// apply_tenant_filter appends the configured tenant filter condition to `where`.
405pub fn apply_tenant_filter(table Table, where QueryData) QueryData {
406 if !tenant_filter_state.enabled || !tenant_filter_state.has_current_tenant {
407 return where
408 }
409 if table_ignores_tenant_filter(table) {
410 return where
411 }
412 tenant_field_name := table_tenant_filter_field_name(table)
413 if tenant_field_name == '' || tenant_field_name in where.fields {
414 return where
415 }
416 mut where_with_tenant := clone_query_data(where)
417 original_fields_len := where_with_tenant.fields.len
418 if original_fields_len > 1 {
419 // Preserve original WHERE precedence before appending `AND tenant = ...`.
420 where_with_tenant.parentheses << [0, original_fields_len - 1]
421 }
422 if original_fields_len > 0 {
423 where_with_tenant.is_and << true
424 }
425 where_with_tenant.fields << tenant_field_name
426 where_with_tenant.data << tenant_filter_state.current_tenant
427 where_with_tenant.types << tenant_filter_primitive_type(tenant_filter_state.current_tenant)
428 where_with_tenant.kinds << .eq
429 return where_with_tenant
430}
431
432fn tenant_filter_scope_snapshot() TenantFilterScopeState {
433 return TenantFilterScopeState{
434 enabled: tenant_filter_state.enabled
435 has_current_tenant: tenant_filter_state.has_current_tenant
436 current_tenant: tenant_filter_state.current_tenant
437 }
438}
439
440fn tenant_filter_scope_restore(saved TenantFilterScopeState) {
441 tenant_filter_state.enabled = saved.enabled
442 tenant_filter_state.has_current_tenant = saved.has_current_tenant
443 tenant_filter_state.current_tenant = saved.current_tenant
444}
445
446fn normalize_tenant_filter_field_name(field_name string) string {
447 name := trim_attr_arg(field_name)
448 if name == '' {
449 return default_tenant_filter_field_name
450 }
451 return name
452}
453
454fn trim_attr_arg(arg string) string {
455 mut out := arg.trim_space()
456 if out.len >= 2 && ((out.starts_with("'") && out.ends_with("'"))
457 || (out.starts_with('"') && out.ends_with('"'))) {
458 out = out[1..out.len - 1].trim_space()
459 }
460 return out
461}
462
463fn tenant_filter_array_primitive_type[T](value []T) int {
464 if value.len > 0 {
465 first := value[0]
466 return tenant_filter_primitive_type(Primitive(first))
467 }
468 return type_idx['int']
469}
470
471fn tenant_filter_primitive_type(value Primitive) int {
472 return match value {
473 bool {
474 type_idx['bool']
475 }
476 i8 {
477 type_idx['i8']
478 }
479 i16 {
480 type_idx['i16']
481 }
482 int {
483 type_idx['int']
484 }
485 i64 {
486 type_idx['i64']
487 }
488 u8 {
489 type_idx['u8']
490 }
491 u16 {
492 type_idx['u16']
493 }
494 u32 {
495 type_idx['u32']
496 }
497 u64 {
498 type_idx['u64']
499 }
500 f32 {
501 type_idx['f32']
502 }
503 f64 {
504 type_idx['f64']
505 }
506 string {
507 type_string
508 }
509 time.Time {
510 time_
511 }
512 Null {
513 type_idx['int']
514 }
515 InfixType {
516 tenant_filter_primitive_type(value.right)
517 }
518 []Primitive {
519 if value.len > 0 {
520 tenant_filter_primitive_type(value[0])
521 } else {
522 type_idx['int']
523 }
524 }
525 []bool {
526 tenant_filter_array_primitive_type(value)
527 }
528 []f32 {
529 tenant_filter_array_primitive_type(value)
530 }
531 []f64 {
532 tenant_filter_array_primitive_type(value)
533 }
534 []i16 {
535 tenant_filter_array_primitive_type(value)
536 }
537 []i64 {
538 tenant_filter_array_primitive_type(value)
539 }
540 []i8 {
541 tenant_filter_array_primitive_type(value)
542 }
543 []int {
544 tenant_filter_array_primitive_type(value)
545 }
546 []string {
547 tenant_filter_array_primitive_type(value)
548 }
549 []time.Time {
550 tenant_filter_array_primitive_type(value)
551 }
552 []u16 {
553 tenant_filter_array_primitive_type(value)
554 }
555 []u32 {
556 tenant_filter_array_primitive_type(value)
557 }
558 []u64 {
559 tenant_filter_array_primitive_type(value)
560 }
561 []u8 {
562 tenant_filter_array_primitive_type(value)
563 }
564 []InfixType {
565 tenant_filter_array_primitive_type(value)
566 }
567 }
568}
569
570fn table_tenant_filter_field_name(table Table) string {
571 mut field_name := tenant_filter_state.field_name
572 for attr in table.attrs {
573 if attr_name_matches(attr.name, tenant_field_attr_name) && attr.has_arg {
574 override_field_name := trim_attr_arg(attr.arg)
575 if override_field_name != '' {
576 field_name = override_field_name
577 }
578 }
579 }
580 return normalize_tenant_filter_field_name(field_name)
581}
582
583fn table_ignores_tenant_filter(table Table) bool {
584 for attr in table.attrs {
585 if attr_name_matches(attr.name, ignore_tenant_filter_attr_name) {
586 if !attr.has_arg {
587 return true
588 }
589 if is_enabled := parse_bool_attr(attr.arg) {
590 return is_enabled
591 }
592 return true
593 }
594 if attr_name_matches(attr.name, tenant_filter_attr_name) && attr.has_arg {
595 if is_enabled := parse_bool_attr(attr.arg) {
596 return !is_enabled
597 }
598 }
599 }
600 return false
601}
602
603fn attr_name_matches(name string, expected string) bool {
604 return name == expected || name.ends_with('.${expected}')
605}
606
607fn parse_bool_attr(raw string) ?bool {
608 value := trim_attr_arg(raw).to_lower()
609 return match value {
610 '1', 'true', 'yes', 'on' {
611 true
612 }
613 '0', 'false', 'no', 'off' {
614 false
615 }
616 else {
617 none
618 }
619 }
620}
621
622fn clone_query_data(data QueryData) QueryData {
623 return QueryData{
624 fields: data.fields.clone()
625 data: data.data.clone()
626 types: data.types.clone()
627 parentheses: data.parentheses.map(it.clone())
628 kinds: data.kinds.clone()
629 auto_fields: data.auto_fields.clone()
630 is_and: data.is_and.clone()
631 batch_rows: data.batch_rows
632 batch_key: data.batch_key
633 }
634}
635
636// Generates an sql stmt, from universal parameter
637// q - The quotes character, which can be different in every type, so it's variable
638// num - Stmt uses nums at prepared statements (? or ?1)
639// qm - Character for prepared statement (qm for question mark, as in sqlite)
640// start_pos - When num is true, it's the start position of the counter
641pub fn orm_stmt_gen(sql_dialect SQLDialect, table Table, q string, kind StmtKind, num bool, qm string,
642 start_pos int, data QueryData, where QueryData) (string, QueryData) {
643 mut str := ''
644 mut c := start_pos
645 insert_data := prepare_insert_query_data(data)
646
647 match kind {
648 .insert {
649 row_count := if insert_data.batch_rows > 0 { insert_data.batch_rows } else { 1 }
650 mut values := []string{}
651 mut select_fields := []string{}
652 are_values_empty := insert_data.fields.len == 0
653
654 for column_name in insert_data.fields {
655 select_fields << '${q}${column_name}${q}'
656 }
657 if !are_values_empty {
658 for _ in 0 .. row_count {
659 mut row_values := []string{}
660 for _ in insert_data.fields {
661 row_values << factory_insert_qm_value(num, qm, c)
662 c++
663 }
664 values << '(${row_values.join(', ')})'
665 }
666 }
667
668 str += 'INSERT INTO ${q}${table.name}${q} '
669
670 if are_values_empty {
671 if row_count == 1 && sql_dialect in [.sqlite, .pg, .h2] {
672 str += 'DEFAULT VALUES'
673 } else {
674 str += '() VALUES '
675 str += []string{len: row_count, init: '()'}.join(', ')
676 }
677 } else {
678 str += '('
679 str += select_fields.join(', ')
680 str += ') VALUES '
681 str += values.join(', ')
682 }
683 }
684 .update {
685 str += 'UPDATE ${q}${table.name}${q} SET '
686 if data.batch_rows > 0 {
687 for i, field in data.fields {
688 str += '${q}${field}${q} = CASE ${q}${data.batch_key}${q} '
689 for _ in 0 .. data.batch_rows {
690 str += 'WHEN ${qm}'
691 if num {
692 str += '${c}'
693 c++
694 }
695 str += ' THEN ${qm}'
696 if num {
697 str += '${c}'
698 c++
699 }
700 str += ' '
701 }
702 str += 'ELSE ${q}${field}${q} END'
703 if i < data.fields.len - 1 {
704 str += ', '
705 }
706 }
707 } else {
708 for i, field in data.fields {
709 str += '${q}${field}${q} = '
710 if data.data.len > i {
711 d := data.data[i]
712 if d is InfixType {
713 op := match d.operator {
714 .add {
715 '+'
716 }
717 .sub {
718 '-'
719 }
720 .mul {
721 '*'
722 }
723 .div {
724 '/'
725 }
726 }
727
728 str += '${d.name} ${op} ${qm}'
729 } else {
730 str += '${qm}'
731 }
732 } else {
733 str += '${qm}'
734 }
735 if num {
736 str += '${c}'
737 c++
738 }
739 if i < data.fields.len - 1 {
740 str += ', '
741 }
742 }
743 }
744 str += ' WHERE '
745 }
746 .delete {
747 str += 'DELETE FROM ${q}${table.name}${q} WHERE '
748 }
749 }
750
751 // where
752 if kind == .update || kind == .delete {
753 str += gen_where_clause(where, q, qm, num, mut &c)
754 }
755 str += ';'
756 $if trace_orm_stmt ? {
757 eprintln('> orm_stmt sql_dialect: ${sql_dialect} | table: ${table.name} | kind: ${kind} | query: ${str}')
758 }
759 $if trace_orm ? {
760 eprintln('> orm: ${str}')
761 }
762 returned_data := if kind == .insert { insert_data } else { data }
763
764 return str, returned_data
765}
766
767fn prepare_insert_query_data(data QueryData) QueryData {
768 mut prepared := QueryData{
769 batch_rows: data.batch_rows
770 batch_key: data.batch_key
771 parentheses: data.parentheses.clone()
772 is_and: data.is_and.clone()
773 }
774 mut included_indexes := []int{}
775 if data.batch_rows > 0 && data.fields.len > 0 {
776 for i, column_name in data.fields {
777 mut skip_auto_field := i in data.auto_fields
778 if skip_auto_field {
779 for row in 0 .. data.batch_rows {
780 data_idx := row * data.fields.len + i
781 if data_idx >= data.data.len
782 || !should_skip_insert_auto_field(data.data[data_idx]) {
783 skip_auto_field = false
784 break
785 }
786 }
787 }
788 if skip_auto_field {
789 continue
790 }
791 prepared.fields << column_name
792 if i < data.types.len {
793 prepared.types << data.types[i]
794 }
795 if i < data.kinds.len {
796 prepared.kinds << data.kinds[i]
797 }
798 if i in data.auto_fields {
799 prepared.auto_fields << prepared.fields.len - 1
800 }
801 included_indexes << i
802 }
803 for row in 0 .. data.batch_rows {
804 for i in included_indexes {
805 data_idx := row * data.fields.len + i
806 if data_idx < data.data.len {
807 prepared.data << data.data[data_idx]
808 }
809 }
810 }
811 return prepared
812 }
813 for i, column_name in data.fields {
814 if i >= data.data.len {
815 prepared.fields << column_name
816 if i < data.types.len {
817 prepared.types << data.types[i]
818 }
819 if i < data.kinds.len {
820 prepared.kinds << data.kinds[i]
821 }
822 if i in data.auto_fields {
823 prepared.auto_fields << prepared.fields.len - 1
824 }
825 continue
826 }
827 if i in data.auto_fields && should_skip_insert_auto_field(data.data[i]) {
828 continue
829 }
830 prepared.fields << column_name
831 prepared.data << data.data[i]
832 if i < data.types.len {
833 prepared.types << data.types[i]
834 }
835 if i < data.kinds.len {
836 prepared.kinds << data.kinds[i]
837 }
838 if i in data.auto_fields {
839 prepared.auto_fields << prepared.fields.len - 1
840 }
841 }
842 return prepared
843}
844
845fn should_skip_insert_auto_field(value Primitive) bool {
846 mut x := value
847 return match mut x {
848 Null { true }
849 string { x == '' }
850 i8, i16, int, i64, u8, u16, u32, u64 { u64(x) == 0 }
851 f32, f64 { f64(x) == 0 }
852 time.Time { x == time.Time{} }
853 bool { !x }
854 else { false }
855 }
856}
857
858fn build_upsert_where(data QueryData, conflict_groups [][]string) !QueryData {
859 mut field_indexes := map[string]int{}
860 for i, field in data.fields {
861 field_indexes[field] = i
862 }
863 mut where := QueryData{}
864 for group in conflict_groups {
865 if group.len == 0 {
866 continue
867 }
868 start := where.fields.len
869 if start > 0 {
870 where.is_and << false
871 }
872 for i, field_name in group {
873 idx := field_indexes[field_name] or {
874 return error('${@FN}(): missing conflict field `${field_name}` in upsert data')
875 }
876 if idx >= data.data.len {
877 return error('${@FN}(): missing conflict value for `${field_name}` in upsert data')
878 }
879 where.fields << field_name
880 where.data << data.data[idx]
881 where.kinds << .eq
882 if i > 0 {
883 where.is_and << true
884 }
885 }
886 if group.len > 1 {
887 where.parentheses << [start, where.fields.len - 1]
888 }
889 }
890 return where
891}
892
893fn upsert_conflict_groups(data QueryData, conflict_groups [][]string) [][]string {
894 mut present_fields := map[string]bool{}
895 for field in data.fields {
896 present_fields[field] = true
897 }
898 mut usable := [][]string{}
899 for group in conflict_groups {
900 if group.len == 0 {
901 continue
902 }
903 mut ok := true
904 for field_name in group {
905 if field_name !in present_fields {
906 ok = false
907 break
908 }
909 }
910 if ok {
911 usable << group
912 }
913 }
914 return usable
915}
916
917pub struct UpsertData {
918pub:
919 valid bool
920pub mut:
921 insert_data QueryData
922 where QueryData
923}
924
925// prepare_upsert resolves the filtered insert data and the conflict `WHERE` clause for an upsert.
926pub fn prepare_upsert(data QueryData, conflict_groups [][]string) UpsertData {
927 insert_data := prepare_insert_query_data(data)
928 usable_groups := upsert_conflict_groups(insert_data, conflict_groups)
929 if usable_groups.len == 0 {
930 return UpsertData{
931 insert_data: insert_data
932 }
933 }
934 where := build_upsert_where(insert_data, usable_groups) or {
935 return UpsertData{
936 insert_data: insert_data
937 }
938 }
939 return UpsertData{
940 valid: true
941 insert_data: insert_data
942 where: where
943 }
944}
945
946// upsert_count converts a `select count(*)` ORM result into an integer count.
947pub fn upsert_count(result [][]Primitive) int {
948 if result.len == 0 || result[0].len == 0 {
949 return 0
950 }
951 count_val := result[0][0]
952 return match count_val {
953 int { count_val }
954 i64 { int(count_val) }
955 u64 { int(count_val) }
956 else { 0 }
957 }
958}
959
960// upsert_missing_conflict_error returns the standard missing-conflict error for SQL upserts.
961pub fn upsert_missing_conflict_error(table Table) ! {
962 return error('upsert(): table `${table.name}` needs at least one primary or unique field with a concrete value')
963}
964
965// upsert_ambiguous_error returns the standard ambiguous-match error for SQL upserts.
966pub fn upsert_ambiguous_error(table Table) ! {
967 return error('upsert(): upsert on table `${table.name}` matched multiple rows')
968}
969
970// Generates an sql select stmt, from universal parameter
971// orm - See SelectConfig
972// q, num, qm, start_pos - see orm_stmt_gen
973// where - See QueryData
974pub fn orm_select_gen(cfg SelectConfig, q string, num bool, qm string, start_pos int, where QueryData) string {
975 mut str := 'SELECT '
976
977 if cfg.has_distinct {
978 str += 'DISTINCT '
979 }
980
981 if cfg.aggregate_kind != .none {
982 if cfg.aggregate_kind == .count {
983 str += cfg.aggregate_kind.to_str()
984 } else {
985 str += '${cfg.aggregate_kind.to_str()}(${q}${cfg.aggregate_field}${q})'
986 }
987 } else {
988 for i, field in cfg.fields {
989 select_expr := if cfg.select_exprs.len > i && cfg.select_exprs[i] != '' {
990 cfg.select_exprs[i]
991 } else {
992 field
993 }
994 if select_expr == field {
995 str += '${q}${field}${q}'
996 } else {
997 str += select_expr
998 }
999 if i < cfg.fields.len - 1 {
1000 str += ', '
1001 }
1002 }
1003 }
1004
1005 str += ' FROM ${q}${cfg.table.name}${q}'
1006
1007 // Generate JOIN clauses
1008 for join in cfg.joins {
1009 str += ' ${join.kind.to_str()} ${q}${join.table.name}${q}'
1010 str += ' ON ${q}${cfg.table.name}${q}.${q}${join.on_left_col}${q}'
1011 str += ' = ${q}${join.table.name}${q}.${q}${join.on_right_col}${q}'
1012 }
1013
1014 mut c := start_pos
1015
1016 if cfg.has_where {
1017 str += ' WHERE '
1018 $if trace_orm_where ? {
1019 eprintln('> orm_select_gen: where.fields.len = ${where.fields.len}')
1020 eprintln('> orm_select_gen: where.kinds.len = ${where.kinds.len}')
1021 for i, field in where.fields {
1022 eprintln('> orm_select_gen: field[${i}] = ${field}')
1023 }
1024 }
1025 str += gen_where_clause(where, q, qm, num, mut &c)
1026 }
1027
1028 // Note: do not order, if the user did not want it explicitly,
1029 // ordering is *slow*, especially if there are no indexes!
1030 if cfg.has_order {
1031 str += ' ORDER BY '
1032 str += '${q}${cfg.order}${q} '
1033 str += cfg.order_type.to_str()
1034 }
1035
1036 if cfg.has_limit {
1037 str += ' LIMIT ${qm}'
1038 if num {
1039 str += '${c}'
1040 c++
1041 }
1042 }
1043
1044 if cfg.has_offset {
1045 str += ' OFFSET ${qm}'
1046 if num {
1047 str += '${c}'
1048 c++
1049 }
1050 }
1051
1052 str += ';'
1053 $if trace_orm_query ? {
1054 eprintln('> orm_query: ${str}')
1055 }
1056 $if trace_orm ? {
1057 eprintln('> orm: ${str}')
1058 }
1059 return str
1060}
1061
1062fn gen_where_clause(where QueryData, q string, qm string, num bool, mut c &int) string {
1063 mut str := ''
1064
1065 for i, field in where.fields {
1066 current_pre_par := where.parentheses.count(it[0] == i)
1067 current_post_par := where.parentheses.count(it[1] == i)
1068
1069 if current_pre_par > 0 {
1070 str += ' ( '.repeat(current_pre_par)
1071 }
1072 str += '${q}${field}${q} ${where.kinds[i].to_str()}'
1073 if !where.kinds[i].is_unary() {
1074 if where.data.len > i && where.data[i] is []Primitive {
1075 len := (where.data[i] as []Primitive).len
1076 mut tmp := []string{len: len}
1077 for j in 0 .. len {
1078 tmp[j] = '${qm}'
1079 if num {
1080 tmp[j] += '${c}'
1081 c++
1082 }
1083 }
1084 str += ' (${tmp.join(', ')})'
1085 } else {
1086 str += ' ${qm}'
1087 if num {
1088 str += '${c}'
1089 c++
1090 }
1091 }
1092 }
1093 if current_post_par > 0 {
1094 str += ' ) '.repeat(current_post_par)
1095 }
1096 if i < where.fields.len - 1 {
1097 if where.is_and[i] {
1098 str += ' AND '
1099 } else {
1100 str += ' OR '
1101 }
1102 }
1103 }
1104 return str
1105}
1106
1107// Generates an sql table stmt, from universal parameter
1108// table - Table struct
1109// q - see orm_stmt_gen
1110// defaults - enables default values in stmt
1111// def_unique_len - sets default unique length for texts
1112// fields - See TableField
1113// sql_from_v - Function which maps type indices to sql type names
1114// alternative - Needed for msdb
1115fn parse_table_attr_fields(table Table, attr VAttribute, valid_sql_field_names []string) ![]string {
1116 if attr.arg == '' || attr.kind != .string {
1117 return error("${attr.name} attribute needs to be in the format [${attr.name}: 'f1, f2, f3']")
1118 }
1119 mut attr_fields := []string{}
1120 for raw_field_name in attr.arg.split(',') {
1121 field_name := raw_field_name.trim_space()
1122 if field_name == '' {
1123 return error("${attr.name} attribute needs to be in the format [${attr.name}: 'f1, f2, f3']")
1124 }
1125 if field_name !in valid_sql_field_names {
1126 return error("table `${table.name}` has no field's name: `${field_name}`")
1127 }
1128 if field_name !in attr_fields {
1129 attr_fields << field_name
1130 }
1131 }
1132 return attr_fields
1133}
1134
1135pub fn orm_table_gen(sql_dialect SQLDialect, table Table, q string, defaults bool, def_unique_len int, fields []TableField, sql_from_v fn (int) !string,
1136 alternative bool) !string {
1137 mut str := 'CREATE TABLE IF NOT EXISTS ${q}${table.name}${q} ('
1138
1139 if alternative {
1140 str = 'IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${q}${table.name}${q} and xtype=${q}U${q}) CREATE TABLE ${q}${table.name}${q} ('
1141 }
1142
1143 mut fs := []string{}
1144 mut unique_fields := []string{}
1145 mut unique := map[string][]string{}
1146 mut primary := ''
1147 mut primary_typ := 0
1148 mut table_comment := ''
1149 mut field_comments := map[string]string{}
1150 mut index_fields := []string{}
1151 mut unique_key_fields := [][]string{}
1152
1153 valid_sql_field_names := fields.map(sql_field_name(it))
1154
1155 for attr in table.attrs {
1156 match attr.name {
1157 'comment' {
1158 if attr.arg != '' && attr.kind == .string {
1159 table_comment = attr.arg.replace('"', '\\"')
1160 }
1161 }
1162 'index' {
1163 attr_fields := parse_table_attr_fields(table, attr, valid_sql_field_names) or {
1164 return err
1165 }
1166 for field_name in attr_fields {
1167 if field_name !in index_fields {
1168 index_fields << field_name
1169 }
1170 }
1171 }
1172 'unique_key' {
1173 attr_fields := parse_table_attr_fields(table, attr, valid_sql_field_names) or {
1174 return err
1175 }
1176 if attr_fields.len > 0 {
1177 unique_key_fields << attr_fields
1178 }
1179 }
1180 else {}
1181 }
1182 }
1183
1184 for field in fields {
1185 if field.is_arr {
1186 continue
1187 }
1188 mut default_val := field.default_val
1189 mut has_default := default_val != ''
1190 mut nullable := field.nullable
1191 mut is_unique := false
1192 mut is_skip := false
1193 mut unique_len := 0
1194 mut references_table := ''
1195 mut references_field := ''
1196 mut field_comment := ''
1197 mut field_name := sql_field_name(field)
1198 mut col_typ := sql_from_v(sql_field_type(field)) or {
1199 // Struct fields are treated as foreign key references, which requires a primary key
1200 if primary_typ == 0 {
1201 return error('struct field `${field_name}` in table `${table.name}` requires a primary key field for foreign key reference - add a field with [primary] attribute or use [sql: \'-\'] to skip this field')
1202 }
1203 field_name = '${field_name}_id'
1204 sql_from_v(primary_typ)!
1205 }
1206 for attr in field.attrs {
1207 match attr.name {
1208 'sql' {
1209 // [sql:'-']
1210 if attr.arg == '-' {
1211 is_skip = true
1212 }
1213 }
1214 'primary' {
1215 primary = field_name
1216 primary_typ = field.typ
1217 }
1218 'unique' {
1219 if attr.arg != '' {
1220 if attr.kind == .string {
1221 if attr.arg !in unique {
1222 unique[attr.arg] = []string{}
1223 }
1224 unique[attr.arg] << field_name
1225 continue
1226 } else if attr.kind == .number {
1227 unique_len = attr.arg.int()
1228 is_unique = true
1229 continue
1230 }
1231 }
1232 is_unique = true
1233 }
1234 'skip' {
1235 is_skip = true
1236 }
1237 'sql_type' {
1238 col_typ = attr.arg.str()
1239 }
1240 'default' {
1241 has_default = true
1242 if default_val == '' {
1243 default_val = attr.arg.str()
1244 }
1245 }
1246 'references' {
1247 nullable = true
1248 if attr.arg == '' {
1249 if field.name.ends_with('_id') {
1250 references_table = field.name.trim_right('_id')
1251 references_field = 'id'
1252 } else {
1253 return error("references attribute can only be implicit if the field name ends with '_id'")
1254 }
1255 } else {
1256 if attr.arg.trim(' ') == '' {
1257 return error("references attribute needs to be in the format [references], [references: 'tablename'], or [references: 'tablename(field_id)']")
1258 }
1259 if attr.arg.contains('(') {
1260 if ref_table, ref_field := attr.arg.split_once('(') {
1261 if !ref_field.ends_with(')') {
1262 return error("explicit references attribute should be written as [references: 'tablename(field_id)']")
1263 }
1264 references_table = ref_table
1265 references_field = ref_field[..ref_field.len - 1]
1266 }
1267 } else {
1268 references_table = attr.arg
1269 references_field = 'id'
1270 }
1271 }
1272 }
1273 'comment' {
1274 if attr.arg != '' && attr.kind == .string {
1275 field_comment = attr.arg.replace("'", "\\'")
1276 field_comments[field_name] = field_comment
1277 }
1278 }
1279 'index' {
1280 if field_name !in index_fields {
1281 index_fields << field_name
1282 }
1283 }
1284 else {}
1285 }
1286 }
1287 if is_skip {
1288 continue
1289 }
1290 mut stmt := ''
1291 if col_typ == '' {
1292 return error('Unknown type (${field.typ}) for field ${field.name} in struct ${table.name}')
1293 }
1294 stmt = '${q}${field_name}${q} ${col_typ}'
1295 if defaults && has_default {
1296 if default_val != '' {
1297 stmt += ' DEFAULT ${default_val}'
1298 } else {
1299 // Handle @[default: ''] - explicitly set DEFAULT '' for the column
1300 stmt += " DEFAULT ''"
1301 }
1302 }
1303 if sql_dialect == .mysql && field_comment != '' {
1304 stmt += " COMMENT '${field_comment}'"
1305 }
1306 if !nullable {
1307 stmt += ' NOT NULL'
1308 }
1309 if is_unique {
1310 mut f := 'UNIQUE(${q}${field_name}${q}'
1311 if col_typ == 'TEXT' && def_unique_len > 0 {
1312 if unique_len > 0 {
1313 f += '(${unique_len})'
1314 } else {
1315 f += '(${def_unique_len})'
1316 }
1317 }
1318 f += ')'
1319 unique_fields << f
1320 }
1321 if references_table != '' {
1322 stmt += ' REFERENCES ${q}${references_table}${q}(${q}${references_field}${q})'
1323 }
1324 fs << stmt
1325 }
1326
1327 if unique.len > 0 {
1328 for k, v in unique {
1329 mut tmp := []string{}
1330 for f in v {
1331 tmp << '${q}${f}${q}'
1332 }
1333 fs << '/* ${k} */UNIQUE(${tmp.join(', ')})'
1334 }
1335 }
1336 for key_fields in unique_key_fields {
1337 mut tmp := []string{}
1338 for field_name in key_fields {
1339 tmp << '${q}${field_name}${q}'
1340 }
1341 fs << 'UNIQUE(${tmp.join(', ')})'
1342 }
1343
1344 if primary != '' {
1345 fs << 'PRIMARY KEY(${q}${primary}${q})'
1346 }
1347
1348 fs << unique_fields
1349 unique_fields.clear() // ownership transferred to fs to avoid double-free under -autofree
1350 str += fs.join(', ')
1351 if index_fields.len > 0 && sql_dialect == .mysql {
1352 str += ', INDEX `idx_${table.name}` (`'
1353 str += index_fields.join('`,`')
1354 str += '`)'
1355 }
1356 str += ')'
1357 if sql_dialect == .mysql && table_comment != '' {
1358 str += " COMMENT = '${table_comment}'"
1359 }
1360 str += ';'
1361
1362 if sql_dialect in [.pg, .h2] {
1363 if table_comment != '' {
1364 str += "\nCOMMENT ON TABLE \"${table.name}\" IS '${table_comment}';"
1365 }
1366 for f, c in field_comments {
1367 str += "\nCOMMENT ON COLUMN \"${table.name}\".\"${f}\" IS '${c}';"
1368 }
1369 }
1370 if sql_dialect in [.pg, .sqlite, .h2] && index_fields.len > 0 {
1371 str += '\nCREATE INDEX "idx_${table.name}" ON "${table.name}" ("'
1372 str += index_fields.join('","')
1373 str += '");'
1374 }
1375 $if trace_orm_create ? {
1376 eprintln('> orm_create table: ${table.name} | query: ${str}')
1377 }
1378 $if trace_orm ? {
1379 eprintln('> orm: ${str}')
1380 }
1381
1382 return str
1383}
1384
1385// Get's the sql field type
1386fn sql_field_type(field TableField) int {
1387 mut typ := field.typ
1388 for attr in field.attrs {
1389 // @[serial]
1390 if attr.name == 'serial' && attr.kind == .plain && !attr.has_arg {
1391 typ = serial
1392 break
1393 }
1394
1395 if attr.kind == .plain && attr.name == 'sql' && attr.arg != '' {
1396 // @[sql: serial]
1397 if attr.arg.to_lower() == 'serial' {
1398 typ = serial
1399 break
1400 }
1401 typ = type_idx[attr.arg]
1402 break
1403 }
1404 }
1405 return typ
1406}
1407
1408// Get's the sql field name
1409fn sql_field_name(field TableField) string {
1410 mut name := field.name
1411 for attr in field.attrs {
1412 if attr.name == 'sql' && attr.has_arg && attr.kind == .string {
1413 name = attr.arg
1414 break
1415 }
1416 }
1417 return name
1418}
1419
1420// Get's the SQL select expression for a field.
1421fn sql_field_select_expr(field TableField) string {
1422 for attr in field.attrs {
1423 if attr.name == 'sql_select' && attr.has_arg {
1424 return trim_attr_arg(attr.arg)
1425 }
1426 }
1427 return sql_field_name(field)
1428}
1429
1430// needed for backend functions
1431
1432fn bool_to_primitive(b bool) Primitive {
1433 return Primitive(b)
1434}
1435
1436fn option_bool_to_primitive(b ?bool) Primitive {
1437 return if b_ := b { Primitive(b_) } else { null_primitive }
1438}
1439
1440fn array_bool_to_primitive(b []bool) Primitive {
1441 return Primitive(b.map(bool_to_primitive(it)))
1442}
1443
1444fn f32_to_primitive(b f32) Primitive {
1445 return Primitive(b)
1446}
1447
1448fn option_f32_to_primitive(b ?f32) Primitive {
1449 return if b_ := b { Primitive(b_) } else { null_primitive }
1450}
1451
1452fn array_f32_to_primitive(b []f32) Primitive {
1453 return Primitive(b.map(f32_to_primitive(it)))
1454}
1455
1456fn f64_to_primitive(b f64) Primitive {
1457 return Primitive(b)
1458}
1459
1460fn option_f64_to_primitive(b ?f64) Primitive {
1461 return if b_ := b { Primitive(b_) } else { null_primitive }
1462}
1463
1464fn array_f64_to_primitive(b []f64) Primitive {
1465 return Primitive(b.map(f64_to_primitive(it)))
1466}
1467
1468fn i8_to_primitive(b i8) Primitive {
1469 return Primitive(b)
1470}
1471
1472fn option_i8_to_primitive(b ?i8) Primitive {
1473 return if b_ := b { Primitive(b_) } else { null_primitive }
1474}
1475
1476fn array_i8_to_primitive(b []i8) Primitive {
1477 return Primitive(b.map(i8_to_primitive(it)))
1478}
1479
1480fn i16_to_primitive(b i16) Primitive {
1481 return Primitive(b)
1482}
1483
1484fn option_i16_to_primitive(b ?i16) Primitive {
1485 return if b_ := b { Primitive(b_) } else { null_primitive }
1486}
1487
1488fn array_i16_to_primitive(b []i16) Primitive {
1489 return Primitive(b.map(i16_to_primitive(it)))
1490}
1491
1492fn int_to_primitive(b int) Primitive {
1493 return Primitive(b)
1494}
1495
1496fn option_int_to_primitive(b ?int) Primitive {
1497 return if b_ := b { Primitive(b_) } else { null_primitive }
1498}
1499
1500fn array_int_to_primitive(b []int) Primitive {
1501 return Primitive(b.map(int_to_primitive(it)))
1502}
1503
1504// int_literal_to_primitive handles int literal value
1505fn int_literal_to_primitive(b int) Primitive {
1506 return Primitive(b)
1507}
1508
1509fn option_int_literal_to_primitive(b ?int) Primitive {
1510 return if b_ := b { Primitive(b_) } else { null_primitive }
1511}
1512
1513fn array_int_literal_to_primitive(b []int) Primitive {
1514 return Primitive(b.map(int_literal_to_primitive(it)))
1515}
1516
1517// float_literal_to_primitive handles float literal value
1518fn float_literal_to_primitive(b f64) Primitive {
1519 return Primitive(b)
1520}
1521
1522fn option_float_literal_to_primitive(b ?f64) Primitive {
1523 return if b_ := b { Primitive(b_) } else { null_primitive }
1524}
1525
1526fn array_float_literal_to_primitive(b []f64) Primitive {
1527 return Primitive(b.map(float_literal_to_primitive(it)))
1528}
1529
1530fn i64_to_primitive(b i64) Primitive {
1531 return Primitive(b)
1532}
1533
1534fn option_i64_to_primitive(b ?i64) Primitive {
1535 return if b_ := b { Primitive(b_) } else { null_primitive }
1536}
1537
1538fn array_i64_to_primitive(b []i64) Primitive {
1539 return Primitive(b.map(i64_to_primitive(it)))
1540}
1541
1542fn u8_to_primitive(b u8) Primitive {
1543 return Primitive(b)
1544}
1545
1546fn option_u8_to_primitive(b ?u8) Primitive {
1547 return if b_ := b { Primitive(b_) } else { null_primitive }
1548}
1549
1550fn array_u8_to_primitive(b []u8) Primitive {
1551 return Primitive(b.map(u8_to_primitive(it)))
1552}
1553
1554fn u16_to_primitive(b u16) Primitive {
1555 return Primitive(b)
1556}
1557
1558fn option_u16_to_primitive(b ?u16) Primitive {
1559 return if b_ := b { Primitive(b_) } else { null_primitive }
1560}
1561
1562fn array_u16_to_primitive(b []u16) Primitive {
1563 return Primitive(b.map(u16_to_primitive(it)))
1564}
1565
1566fn u32_to_primitive(b u32) Primitive {
1567 return Primitive(b)
1568}
1569
1570fn option_u32_to_primitive(b ?u32) Primitive {
1571 return if b_ := b { Primitive(b_) } else { null_primitive }
1572}
1573
1574fn array_u32_to_primitive(b []u32) Primitive {
1575 return Primitive(b.map(u32_to_primitive(it)))
1576}
1577
1578fn u64_to_primitive(b u64) Primitive {
1579 return Primitive(b)
1580}
1581
1582fn option_u64_to_primitive(b ?u64) Primitive {
1583 return if b_ := b { Primitive(b_) } else { null_primitive }
1584}
1585
1586fn array_u64_to_primitive(b []u64) Primitive {
1587 return Primitive(b.map(u64_to_primitive(it)))
1588}
1589
1590fn string_to_primitive(b string) Primitive {
1591 return Primitive(b)
1592}
1593
1594fn option_string_to_primitive(b ?string) Primitive {
1595 return if b_ := b { Primitive(b_) } else { null_primitive }
1596}
1597
1598fn array_string_to_primitive(b []string) Primitive {
1599 return Primitive(b.map(string_to_primitive(it)))
1600}
1601
1602fn time_to_primitive(b time.Time) Primitive {
1603 return Primitive(b)
1604}
1605
1606fn option_time_to_primitive(b ?time.Time) Primitive {
1607 return if b_ := b { Primitive(b_) } else { null_primitive }
1608}
1609
1610fn array_time_to_primitive(b []time.Time) Primitive {
1611 return Primitive(b.map(time_to_primitive(it)))
1612}
1613
1614fn infix_to_primitive(b InfixType) Primitive {
1615 return Primitive(b)
1616}
1617
1618fn factory_insert_qm_value(num bool, qm string, c int) string {
1619 if num {
1620 return '${qm}${c}'
1621 } else {
1622 return '${qm}'
1623 }
1624}
1625