orm: add function call based builder API for dynamic queries (fix #24178) (#24196)

This commit is contained in:
kbkpbot 2025-04-14 00:18:29 +08:00 committed by GitHub
parent d559a62cfe
commit 5e69315fc2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1059 additions and 3 deletions

View file

@ -156,6 +156,7 @@ const skip_with_fsanitize_memory = [
'vlib/orm/orm_order_by_custom_field_test.v',
'vlib/orm/orm_serial_attribute_test.v',
'vlib/orm/orm_option_subselect_test.v',
'vlib/orm/orm_func_test.v',
'vlib/db/sqlite/sqlite_test.v',
'vlib/db/sqlite/sqlite_orm_test.v',
'vlib/db/sqlite/sqlite_comptime_field_test.v',
@ -246,6 +247,7 @@ const skip_on_ubuntu_musl = [
'vlib/orm/orm_order_by_custom_field_test.v',
'vlib/orm/orm_serial_attribute_test.v',
'vlib/orm/orm_option_subselect_test.v',
'vlib/orm/orm_func_test.v',
'vlib/v/tests/orm_enum_test.v',
'vlib/v/tests/orm_sub_struct_test.v',
'vlib/v/tests/orm_sub_array_struct_test.v',

View file

@ -38,6 +38,8 @@ struct Foo {
- `[fkey: 'parent_id']` sets foreign key for an field which holds an array
## Usage
> [!NOTE]
> For using the Function Call API for `orm`, please check [`Function Call API`](#function-call-api).
Here are a couple example structs showing most of the features outlined above.
@ -246,3 +248,106 @@ fn main() {
}!
}
```
## Function Call API
You can utilize the `Function Call API` to work with `ORM`. It provides the
capability to dynamically construct SQL statements. The Function Call API
supports common operations such as `Create Table`/`Drop Table`/`Insert`/`Delete`/`Update`/`Select`,
and offers convenient yet powerful features for constructing `WHERE` clauses,
`SET` clauses, `SELECT` clauses, and more.
A complete example is available [here](https://github.com/vlang/v/blob/master/vlib/orm/orm_func_test.v).
Below, we illustrate its usage through several examples.
1. Define your struct with the same method definitions as before:
```v ignore
@[table: 'sys_users']
struct User {
id int @[primary;serial]
name string
age int
role string
status int
salary int
title string
score int
created_at ?time.Time @[sql_type: 'TIMESTAMP']
}
```
2. Create a database connection:
```v ignore
mut db := sqlite.connect(':memory:')!
defer { db.close() or {} }
```
3. Create a `QueryBuilder` (which also completes struct mapping):
```v ignore
mut qb := orm.new_query[User](db)
```
4. Create a database table:
```v ignore
qb.create()!
```
5. Insert multiple records into the table:
```v ignore
qb.insert_many(users)!
```
6. Delete records (note: `delete()` must follow `where()`):
```v ignore
qb.where('name = ?','John')!.delete()!
```
7. Query records (you can specify fields of interest via `select`):
```v ignore
// Returns []User with only 'name' populated; other fields are zero values.
only_names := qb.select('name')!.query()!
```
8. Update records (note: `update()` must be placed last):
```v ignore
qb.set('age = ?, title = ?', 71, 'boss')!.where('name = ?','John')!.update()!
```
9. Drop the table:
```v ignore
qb.drop()!
```
10. Chainable method calls:
Most Function Call API support chainable calls, allowing easy method chaining:
```v ignore
final_users :=
qb
.drop()!
.create()!
.insert_many(users)!
.set('name = ?', 'haha')!.where('name = ?', 'Tom')!.update()!
.where('age >= ?', 30)!.delete()!
.query()!
```
11. Writing complex nested `WHERE` clauses:
The API includes a built-in parser to handle intricate `WHERE` clause conditions. For example:
```v ignore
where('created_at IS NULL && ((salary > ? && age < ?) || (role LIKE ?))', 2000, 30, '%employee%')!
```
Note the use of placeholders `?`.
The conditional expressions support logical operators including `AND`, `OR`, `||`, and `&&`.

View file

@ -131,7 +131,7 @@ fn (kind OrderType) to_str() string {
// parentheses defines which fields will be inside ()
// auto_fields are indexes of fields where db should generate a value when absent in an insert
pub struct QueryData {
pub:
pub mut:
fields []string
data []Primitive
types []int
@ -149,7 +149,7 @@ pub:
}
pub struct TableField {
pub:
pub mut:
name string
typ int
nullable bool
@ -170,7 +170,7 @@ pub:
// fields - Fields to select
// types - Types to select
pub struct SelectConfig {
pub:
pub mut:
table string
is_count bool
has_where bool

726
vlib/orm/orm_func.v Normal file
View file

@ -0,0 +1,726 @@
module orm
import time
import strings.textscanner
const operators = ['=', '!=', '<>', '>=', '<=', '>', '<', 'LIKE', 'ILIKE', 'IS NULL', 'IS NOT NULL']!
@[heap]
pub struct QueryBuilder[T] {
pub mut:
meta []TableField
valid_sql_field_names []string
conn Connection
config SelectConfig
data QueryData
where QueryData
}
// new_query create a new query object for struct `T`
pub fn new_query[T](conn Connection) &QueryBuilder[T] {
meta := struct_meta[T]()
return &QueryBuilder[T]{
meta: meta
valid_sql_field_names: meta.map(sql_field_name(it))
conn: conn
config: SelectConfig{
table: table_name_from_struct[T]()
}
data: QueryData{}
where: QueryData{}
}
}
// reset reset a query object, but keep the connection and table name
pub fn (qb_ &QueryBuilder[T]) reset() &QueryBuilder[T] {
mut qb := unsafe { qb_ }
old_table_name := qb.config.table
qb.config = SelectConfig{
table: old_table_name
}
qb.data = QueryData{}
qb.where = QueryData{}
return qb
}
// from vlib/v/gen/c/orm.v write_orm_select()
fn type_from(value string) int {
if ret_type := type_idx[value] {
return ret_type
} else {
if value.contains('time.Time') {
return time_
} else if value.contains('struct') {
return type_idx['int']
} else if value.contains('enum') {
return enum_
}
}
return 0
}
// where create a `where` clause
// valid token in the `condition` include: `field's names`, `operator`, `(`, `)`, `?`, `AND`, `OR`, `||`, `&&`,
// valid `operator` incldue: `=`, `!=`, `<>`, `>=`, `<=`, `>`, `<`, `LIKE`, `ILIKE`, `IS NULL`, `IS NOT NULL`
// example: `where('(a > ? AND b <= ?) OR (c <> ? AND (x = ? OR y = ?))', a, b, c, x, y)`
pub fn (qb_ &QueryBuilder[T]) where(condition string, params ...Primitive) !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
qb.parse_conditions(condition, params)!
qb.config.has_where = true
return qb
}
fn parse_error(msg string, pos int, conds string) ! {
mut m := msg + '\n' + '\t' + conds + '\n\t' + ' '.repeat(pos) + '^\n'
return error(m)
}
enum ParserState {
field
op
qm
}
struct MyTextScanner {
textscanner.TextScanner
mut:
last_tok_start int
}
// next_tok get next token from scanner, skip whitespace
fn (mut ss MyTextScanner) next_tok() string {
mut ret := ''
ss.skip_whitespace()
ss.last_tok_start = ss.pos
// check for longest token first
if ss.input[ss.pos..].starts_with('IS NOT NULL') {
ss.pos += 11
return 'IS NOT NULL'
}
if ss.input[ss.pos..].starts_with('IS NULL') {
ss.pos += 7
return 'IS NULL'
}
if ss.remaining() >= 2 {
two_chars := ss.input[ss.pos..ss.pos + 2]
if two_chars in ['>=', '<=', '<>', '!=', '||', '&&'] {
ss.pos += 2
return two_chars
}
}
if ss.remaining() > 0 {
c := ss.input[ss.pos]
if c in [`>`, `<`, `=`] {
ss.pos++
return c.ascii_str()
}
}
for ss.remaining() > 0 {
c := u8(ss.next()) // only support ascii now
if c.is_alnum() || c == `_` || c == `$` {
ret += c.ascii_str()
} else {
if ret.len == 0 {
ret = c.ascii_str()
} else {
// already contain a tok
ss.back()
}
break
}
}
return ret
}
// parse_conditions update `qb` by parsing the `conds` string
fn (qb_ &QueryBuilder[T]) parse_conditions(conds string, params []Primitive) ! {
// conditions: '(a > ? AND b <= ?) OR (c <> ? AND (x = ? OR y = ?))'
mut qb := unsafe { qb_ }
if conds.len == 0 {
return error('${@FN}(): empty condition')
}
required_params := conds.count('?')
if required_params != params.len {
parse_error('${@FN}(): condition requires `${required_params}` params but got `${params.len}`',
0, conds)!
}
mut s := MyTextScanner{
input: conds
ilen: conds.len
}
mut state := ParserState.field
mut tok := ''
mut current_field := ''
mut current_op := OperationKind.eq
mut current_is_and := true
mut i := 0
mut paren_stack := []int{}
mut is_first_field := true
for s.remaining() > 0 {
tok = s.next_tok()
match state {
.field {
// only support valid field names
if tok in qb.valid_sql_field_names {
current_field = tok
state = .op
} else if tok == '(' {
paren_stack << qb.where.fields.len
} else if tok == ')' {
if paren_stack.len == 0 {
parse_error('${@FN}: unexpected `)`', s.last_tok_start, conds)!
}
start_pos := paren_stack.pop()
qb.where.parentheses << [start_pos, qb.where.fields.len - 1]
} else {
parse_error("${@FN}: table `${qb.config.table}` has no field's name: `${tok}`",
s.last_tok_start, conds)!
}
}
.op {
current_op = match tok {
'=' {
OperationKind.eq
}
'<>' {
OperationKind.neq
}
'!=' {
OperationKind.neq
}
'>' {
OperationKind.gt
}
'<' {
OperationKind.lt
}
'>=' {
OperationKind.ge
}
'<=' {
OperationKind.le
}
'LIKE' {
OperationKind.orm_like
}
'ILIKE' {
OperationKind.orm_ilike
}
'IS NULL' {
OperationKind.is_null
}
'IS NOT NULL' {
OperationKind.is_not_null
}
else {
parse_error('${@FN}(): unsupported operator: `${tok}`', s.last_tok_start,
conds)!
OperationKind.eq
}
}
if current_op in [.is_null, .is_not_null]! {
qb.where.fields << current_field
qb.where.kinds << current_op
if is_first_field {
is_first_field = false
} else {
// skip first field
qb.where.is_and << current_is_and
}
}
state = .qm
}
.qm {
if tok == '?' {
// finish an expr, update `qb`
qb.where.fields << current_field
qb.where.data << params[i]
qb.where.kinds << current_op
if is_first_field {
is_first_field = false
} else {
// skip first field
qb.where.is_and << current_is_and
}
i++
} else if tok == ')' {
if paren_stack.len == 0 {
parse_error('${@FN}: unexpected `)`', s.last_tok_start, conds)!
}
start_pos := paren_stack.pop()
qb.where.parentheses << [start_pos, qb.where.fields.len - 1]
} else if tok == 'AND' {
current_is_and = true
state = .field
} else if tok == 'OR' {
current_is_and = false
state = .field
} else if tok == '&&' {
current_is_and = true
state = .field
} else if tok == '||' {
current_is_and = false
state = .field
} else {
parse_error('${@FN}(): unexpected `${tok}`, maybe `AND`,`OR`', s.last_tok_start,
conds)!
}
}
}
}
}
// order create a `order` clause
pub fn (qb_ &QueryBuilder[T]) order(order_type OrderType, field string) !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
if field in qb.valid_sql_field_names {
qb.config.has_order = true
qb.config.order = field
qb.config.order_type = order_type
} else {
return error("${@FN}(): table `${qb.config.table}` has no field's name: `${field}`")
}
return qb
}
// limit create a `limit` clause
pub fn (qb_ &QueryBuilder[T]) limit(limit int) !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
if limit > 0 {
qb.config.has_limit = true
qb.data.data << Primitive(limit)
qb.data.types << type_idx['int']
} else {
return error('${@FN}(): `limit` should be a positive integer')
}
return qb
}
// offset create a `offset` clause
pub fn (qb_ &QueryBuilder[T]) offset(offset int) !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
if offset >= 0 {
qb.config.has_offset = true
qb.data.data << Primitive(offset)
qb.data.types << type_idx['int']
} else {
return error('${@FN}(): `offset` should be a integer > 0')
}
return qb
}
// select create a `select` clause
pub fn (qb_ &QueryBuilder[T]) select(fields ...string) !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
for f in fields {
if f !in qb.valid_sql_field_names {
return error("${@FN}(): table `${qb.config.table}` has no field's name: `${f}`")
}
}
qb.config.fields = fields
return qb
}
// set create a `set` clause for `update`
pub fn (qb_ &QueryBuilder[T]) set(assign string, values ...Primitive) !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
if assign.len == 0 {
return error('${@FN}(): empty `set`')
}
required_params := assign.count('?')
if required_params != values.len {
return error('${@FN}(): `set` requires `${required_params}` params but got `${values.len}`')
}
mut fields := []string{}
assign_splits := assign.split_any(',')
for assign_split in assign_splits {
f := assign_split.split_any('=')
if f.len != 2 {
return error('${@FN}(): `set` syntax error, it should look like : `a=?,b=?`')
}
if f[1].trim_space() != '?' {
return error('${@FN}(): `set` syntax error, it should look like : `a=?,b=?`')
}
field := f[0].trim_space()
if field !in qb.valid_sql_field_names {
return error("${@FN}(): table `${qb.config.table}` has no field's name: `${field}`")
}
fields << field
}
qb.data.fields = fields
qb.data.data = values
return qb
}
// table_name_from_struct get table name from struct
fn table_name_from_struct[T]() string {
mut table_name := T.name
$for a in T.attributes {
$if a.name == 'table' && a.has_arg {
table_name = a.arg
}
}
return table_name
}
// struct_meta return a struct's fields info
fn struct_meta[T]() []TableField {
mut meta := []TableField{}
$for field in T.fields {
mut attrs := []VAttribute{}
mut is_skip := false
for attr in field.attrs {
f := attr.split_any(':')
if f.len == 1 {
ff := f[0].trim_space()
if ff == 'skip' {
is_skip = true
}
attrs << VAttribute{
name: ff
}
continue
}
if f.len == 2 {
ff := f[1].trim_space()
if f[0].trim_space() == 'sql' && ff == '-' {
is_skip = true
}
mut kind := AttributeKind.plain
if ff == 'true' || ff == 'false' {
kind = .bool
} else if ff.starts_with('if ') {
kind = .comptime_define
} else if (ff.starts_with("'") && ff.ends_with("'"))
|| (ff.starts_with('"') && ff.ends_with('"')) {
kind = .string
} else if ff.contains_only('0123456789') {
kind = .number
} else if ff !in ['serial', 'i8', 'i16', 'int', 'i64', 'u8', 'u16', 'u32', 'u64',
'f32', 'f64', 'bool', 'string'] {
// @[sql: data_type] need kind = .plain
// @[sql: column_name] need kind = .string
kind = .string
}
attrs << VAttribute{
name: f[0].trim_space()
has_arg: true
arg: ff
kind: kind
}
}
}
if !is_skip {
meta << TableField{
name: field.name
typ: type_from(typeof(field).name)
nullable: field.is_option
attrs: attrs
}
}
}
return meta
}
// map_row map a row result into a struct
fn (qb &QueryBuilder[T]) map_row(row []Primitive) !T {
mut instance := T{}
$for field in T.fields {
mut m := TableField{}
mm := qb.meta.filter(it.name == field.name)
if mm.len != 0 {
m = mm[0]
}
index := qb.config.fields.index(field.name)
if index >= 0 {
value := row[index]
if value == Primitive(Null{}) && m.nullable {
// set to none by default
} else {
$if field.typ is i8 || field.typ is ?i8 {
instance.$(field.name) = value as i8
} $else $if field.typ is i16 || field.typ is ?i16 {
instance.$(field.name) = value as i16
} $else $if field.typ is int || field.typ is ?int {
instance.$(field.name) = value as int
} $else $if field.typ is i64 || field.typ is ?i64 {
instance.$(field.name) = value as i64
} $else $if field.typ is u8 || field.typ is ?u8 {
instance.$(field.name) = value as u8
} $else $if field.typ is u16 || field.typ is ?u16 {
instance.$(field.name) = value as u16
} $else $if field.typ is u32 || field.typ is ?u32 {
instance.$(field.name) = value as u32
} $else $if field.typ is u64 || field.typ is ?u64 {
instance.$(field.name) = value as u64
} $else $if field.typ is f32 || field.typ is ?f32 {
instance.$(field.name) = value as f32
} $else $if field.typ is f64 || field.typ is ?f64 {
instance.$(field.name) = value as f64
} $else $if field.typ is bool || field.typ is ?bool {
instance.$(field.name) = value as bool
} $else $if field.typ is string || field.typ is ?string {
instance.$(field.name) = value as string
} $else $if field.typ is time.Time || field.typ is ?time.Time {
if m.typ == time_ {
instance.$(field.name) = value as time.Time
} else if m.typ == type_string {
instance.$(field.name) = time.parse(value as string)!
}
}
}
}
}
return instance
}
// prepare QueryBuilder, ready for gen SQL
fn (qb_ &QueryBuilder[T]) prepare() ! {
mut qb := unsafe { qb_ }
// check for mismatch `(` and `)`
for p in qb.where.parentheses {
if p[1] == -1 {
return error('${@FN}(): missing `)`')
}
}
// auto fill field's names if not set by `select`
if qb.config.fields.len == 0 {
qb.config.fields = qb.meta.map(sql_field_name(it))
}
if qb.config.types.len == 0 {
// set field's types
mut types := []int{cap: qb.config.fields.len}
for f in qb.config.fields {
for ff in qb.meta {
if sql_field_name(ff) == f {
types << ff.typ
}
}
}
qb.config.types = types
}
}
// query start a query and return result in struct `T`
pub fn (qb_ &QueryBuilder[T]) query() ![]T {
mut qb := unsafe { qb_ }
defer {
qb.reset()
}
qb.prepare()!
rows := qb.conn.select(qb.config, qb.data, qb.where)!
mut result := []T{cap: rows.len}
for row in rows {
result << qb.map_row[T](row)!
}
return result
}
// count start a count query and return result
pub fn (qb_ &QueryBuilder[T]) count() !int {
mut qb := unsafe { qb_ }
defer {
qb.reset()
}
mut count_config := qb.config
count_config.is_count = true
count_config.fields = []
qb.prepare()!
result := qb.conn.select(count_config, qb.data, qb.where)!
if result.len == 0 || result[0].len == 0 {
return 0
}
count_val := result[0][0]
return match count_val {
int { count_val }
i64 { int(count_val) }
u64 { int(count_val) }
else { return error('${@FN}(): invalid count result type') }
}
}
// insert insert a record into the database
pub fn (qb_ &QueryBuilder[T]) insert[T](value T) !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
defer {
qb.reset()
}
qb.insert_many([value])!
return qb
}
// insert_many insert records into the database
pub fn (qb_ &QueryBuilder[T]) insert_many[T](values []T) !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
defer {
qb.reset()
}
qb.prepare()!
if values.len == 0 {
return error('${@FN}(): `insert` need at least one record')
}
for value in values {
new_qb := fill_data_with_struct[T](value, qb.meta)
qb.conn.insert(qb.config.table, new_qb)!
}
return qb
}
fn fill_data_with_struct[T](value T, meta []TableField) QueryData {
mut qb := QueryData{}
$for field in T.fields {
sql_fields := meta.filter(it.name == field.name)
if sql_fields.len == 1 {
sql_f := sql_fields[0]
sql_f_name := sql_field_name(sql_f)
sql_f_type := sql_field_type(sql_f)
if sql_f_type == serial {
// `serial` should be auto field
qb.auto_fields << qb.fields.len
}
qb.fields << sql_f_name
$if field.typ is bool {
qb.data << bool_to_primitive(value.$(field.name))
} $else $if field.typ is ?bool {
qb.data << option_bool_to_primitive(value.$(field.name))
}
$if field.typ is f32 {
qb.data << f32_to_primitive(value.$(field.name))
} $else $if field.typ is ?f32 {
qb.data << option_f32_to_primitive(value.$(field.name))
}
$if field.typ is f64 {
qb.data << f64_to_primitive(value.$(field.name))
} $else $if field.typ is ?f64 {
qb.data << option_f64_to_primitive(value.$(field.name))
}
$if field.typ is i8 {
qb.data << i8_to_primitive(value.$(field.name))
} $else $if field.typ is ?i8 {
qb.data << option_i8_to_primitive(value.$(field.name))
}
$if field.typ is i16 {
qb.data << i16_to_primitive(value.$(field.name))
} $else $if field.typ is ?i16 {
qb.data << option_i16_to_primitive(value.$(field.name))
}
$if field.typ is int {
qb.data << int_to_primitive(value.$(field.name))
} $else $if field.typ is ?int {
qb.data << option_int_to_primitive(value.$(field.name))
}
$if field.typ is i64 {
qb.data << i64_to_primitive(value.$(field.name))
} $else $if field.typ is ?i64 {
qb.data << option_i64_to_primitive(value.$(field.name))
}
$if field.typ is u8 {
qb.data << u8_to_primitive(value.$(field.name))
} $else $if field.typ is ?u8 {
qb.data << option_u8_to_primitive(value.$(field.name))
}
$if field.typ is u16 {
qb.data << u16_to_primitive(value.$(field.name))
} $else $if field.typ is ?u16 {
qb.data << option_u16_to_primitive(value.$(field.name))
}
$if field.typ is u32 {
qb.data << u32_to_primitive(value.$(field.name))
} $else $if field.typ is ?u32 {
qb.data << option_u32_to_primitive(value.$(field.name))
}
$if field.typ is u64 {
qb.data << u64_to_primitive(value.$(field.name))
} $else $if field.typ is ?u64 {
qb.data << option_u64_to_primitive(value.$(field.name))
}
$if field.typ is string {
qb.data << string_to_primitive(value.$(field.name))
} $else $if field.typ is ?string {
qb.data << option_string_to_primitive(value.$(field.name))
} $else $if field.typ is time.Time {
if sql_f_type == type_string {
qb.data << string_to_primitive(value.$(field.name).format_ss())
} else {
qb.data << time_to_primitive(value.$(field.name))
}
} $else $if field.typ is ?time.Time {
if sql_f_type == type_string {
b := value.$(field.name)
if b_ := b {
qb.data << Primitive(b_.format_ss())
} else {
qb.data << null_primitive
}
} else {
qb.data << option_time_to_primitive(value.$(field.name))
}
}
}
}
return qb
}
// update update record(s) in the database
pub fn (qb_ &QueryBuilder[T]) update() !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
defer {
qb.reset()
}
qb.prepare()!
if qb.data.fields.len == 0 {
return error('${@FN}(): `update` need at least one `set` clause')
}
qb.conn.update(qb.config.table, qb.data, qb.where)!
return qb
}
// delete delete record(s) in the database
pub fn (qb_ &QueryBuilder[T]) delete() !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
defer {
qb.reset()
}
qb.prepare()!
qb.conn.delete(qb.config.table, qb.where)!
return qb
}
// create create a table
pub fn (qb_ &QueryBuilder[T]) create() !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
defer {
qb.reset()
}
qb.conn.create(qb.config.table, qb.meta)!
return qb
}
// drop drop a table
pub fn (qb_ &QueryBuilder[T]) drop() !&QueryBuilder[T] {
mut qb := unsafe { qb_ }
defer {
qb.reset()
}
qb.conn.drop(qb.config.table)!
return qb
}
// last_id returns the last inserted id of the db
pub fn (qb_ &QueryBuilder[T]) last_id() int {
mut qb := unsafe { qb_ }
qb.reset()
return qb.conn.last_id()
}

223
vlib/orm/orm_func_test.v Normal file
View file

@ -0,0 +1,223 @@
import orm
import db.sqlite
import time
@[table: 'sys_users']
struct User {
id int @[primary; serial]
name string
age int
role string
status int
salary int
title string
score int
created_at ?time.Time @[sql_type: 'TIMESTAMP']
}
fn test_orm_func_where() {
mut db := sqlite.connect(':memory:')!
defer { db.close() or {} }
mut qb := orm.new_query[User](db)
// single_condition
qb.reset()
qb.where('age > ?', 25)!
assert qb.where.fields == ['age']
assert qb.where.kinds == [.gt]
assert qb.where.data == [25]
// chain_condition
qb.reset()
qb.where('age > ?', 25)!.where('salary < ?', 1000)!
assert qb.where.fields == ['age', 'salary']
assert qb.where.kinds == [.gt, .lt]
assert qb.where.data == [25, 1000]
// and_or_combination
qb.reset()
qb.where('name = ? AND status = ? OR role = ? || id = ? && title = ?', 'Alice', 1,
'admin', 1, 'st')!
assert qb.where.fields == ['name', 'status', 'role', 'id', 'title']
assert qb.where.kinds == [.eq, .eq, .eq, .eq, .eq]
assert qb.where.is_and == [true, false, false, true]
// nested_parentheses
qb.reset()
qb.where('(salary >= ? AND (age <= ? OR title LIKE ?))', 50000, 35, '%Manager%')!
assert qb.where.parentheses == [[1, 2], [0, 2]]
// complex_nesting
qb.reset()
qb.where('((age = ? OR (salary > ? AND id < ?)) AND (name LIKE ?))', 1, 2, 3, '%test%')!
assert qb.where.parentheses == [[1, 2], [0, 2], [3, 3], [0, 3]]
}
fn test_orm_func_stmts() {
users := [
User{
name: 'Tom'
age: 30
role: 'admin'
status: 1
salary: 5000
title: 'manager'
score: 90
created_at: time.now()
},
User{
name: 'Alice'
age: 20
role: 'employee'
status: 2
salary: 2000
title: 'doctor'
score: 95
created_at: time.now()
},
User{
name: 'Mars'
age: 40
role: 'employer'
status: 3
salary: 1000
title: 'doctor'
score: 85
created_at: time.now()
},
User{
name: 'Kitty'
age: 18
role: 'employer'
status: 1
salary: 1500
title: 'doctor'
score: 87
created_at: time.now()
},
User{
name: 'Silly'
age: 27
role: 'employer'
status: 5
salary: 2500
title: 'doctor'
score: 81
},
User{
name: 'Smith'
age: 37
role: 'employer'
status: 1
salary: 4500
title: 'doctor'
score: 89
created_at: time.now()
},
User{
name: 'Bob'
age: 26
role: 'employer'
status: 2
salary: 6500
title: 'doctor'
score: 81
created_at: time.now()
},
User{
name: 'Peter'
age: 29
role: 'employer'
status: 1
salary: 3500
title: 'doctor'
score: 80
created_at: time.now()
},
User{
name: 'See'
age: 45
role: 'employer'
status: 2
salary: 8500
title: 'doctor'
score: 82
},
User{
name: 'John'
age: 42
role: 'employer'
status: 1
salary: 10000
title: 'doctor'
score: 88
},
]
mut db := sqlite.connect(':memory:')!
defer { db.close() or {} }
mut qb := orm.new_query[User](db)
// create table
qb.create()!
// insert many records
qb.insert_many(users)!
// select count(*)
mut count := qb.count()!
// last_id
mut last_id := qb.last_id()
assert count == last_id
assert count == users.len
// insert a single record
qb.insert(users[0])!
// select * from table
all_users := qb.query()!
assert all_users.len == users.len + 1
// select `name` from table
only_names := qb.select('name')!.query()!
assert only_names[0].name != ''
assert only_names[0].id == 0
assert only_names[0].age == 0
assert only_names[0].role == ''
assert only_names[0].status == 0
assert only_names[0].salary == 0
assert only_names[0].title == ''
assert only_names[0].score == 0
assert only_names[0].created_at == none
// update
qb.set('age = ?, title = ?', 71, 'boss')!.where('name = ?', 'John')!.update()!
john := qb.where('name = ?', 'John')!.query()!
assert john[0].name == 'John'
assert john[0].age == 71
assert john[0].title == 'boss'
// delete
qb.where('name = ?', 'John')!.delete()!
no_john := qb.where('name = ?', 'John')!.query()!
assert no_john.len == 0
// complex select
selected_users := qb.where('created_at IS NULL && ((salary > ? && age < ?) || (role LIKE ?))',
2000, 30, '%employee%')!.query()!
assert selected_users[0].name == 'Silly'
assert selected_users.len == 1
// chain calls
final_users := qb
.drop()!
.create()!
.insert_many(users)!
.set('name = ?', 'haha')!.where('name = ?', 'Tom')!.update()!
.where('age >= ?', 30)!.delete()!
.order(.asc, 'age')!
.limit(100)!
.query()!
assert final_users.len == 5
assert final_users[0].age == 18
}